MatcherQueryParamsValue,
} from './route-resolver/resolver-abstract'
export {
- MatcherPatternPathDynamic,
MatcherPatternPathStatic,
- MatcherPatternPathStar,
- MatcherPatternPathCustomParams,
- // native param parsers
- PARAM_PARSER_INT,
+ MatcherPatternPathDynamic,
} from './route-resolver/matchers/matcher-pattern'
+
export type {
+ EmptyParams,
MatcherPattern,
MatcherPatternHash,
MatcherPatternPath,
MatcherPatternQuery,
MatcherParamsFormatted,
- EmptyParams,
- ParamParser,
+ MatcherPatternPathDynamic_ParamOptions,
} from './route-resolver/matchers/matcher-pattern'
+export {
+ PARAM_PARSER_INT,
+ type ParamParser,
+ defineParamParser,
+} from './route-resolver/matchers/param-parsers'
+
export { miss, MatchMiss } from './route-resolver/matchers/errors'
// in the new experimental router, there are only parents
--- /dev/null
+import { miss } from './errors'
+import { MatcherPatternPath } from './matcher-pattern'
+
+/**
+ * Allows matching a static path folllowed by anything.
+ *
+ * @example
+ *
+ * ```ts
+ * const matcher = new MatcherPatternPathStar('/team')
+ * matcher.match('/team/123') // { pathMatch: '/123' }
+ * matcher.match('/team/123/more') // { pathMatch: '/123/more' }
+ * matcher.match('/team-123') // { pathMatch: '-123' }
+ * matcher.match('/team') // { pathMatch: '' }
+ * matcher.build({ pathMatch: '/123' }) // '/team/123'
+ * ```
+ */
+export class MatcherPatternPathStar
+ implements MatcherPatternPath<{ pathMatch: string }>
+{
+ private path: string
+ constructor(path: string = '') {
+ this.path = path.toLowerCase()
+ }
+
+ match(path: string): { pathMatch: string } {
+ if (!path.toLowerCase().startsWith(this.path)) {
+ throw miss()
+ }
+ return {
+ pathMatch: path.slice(this.path.length),
+ }
+ }
+
+ build(params: { pathMatch: string }): string {
+ return this.path + params.pathMatch
+ }
+}
import { describe, expect, it } from 'vitest'
import {
MatcherPatternPathStatic,
- MatcherPatternPathStar,
- MatcherPatternPathCustomParams,
+ MatcherPatternPathDynamic,
} from './matcher-pattern'
+import { MatcherPatternPathStar } from './matcher-pattern-path-star'
describe('MatcherPatternPathStatic', () => {
describe('match()', () => {
describe('MatcherPatternPathCustom', () => {
it('single param', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams\/([^/]+?)\/b$/i,
{
// all defaults
})
it('decodes single param', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams\/([^/]+?)$/i,
{
teamId: {},
})
it('optional param', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams(?:\/([^/]+?))?\/b$/i,
{
teamId: {},
})
it('repeatable param', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams\/(.+?)\/b$/i,
{
teamId: { repeat: true },
})
it('repeatable optional param', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams(?:\/(.+?))?\/b$/i,
{
teamId: { repeat: true },
})
it('multiple params', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams\/([^/]+?)\/([^/]+?)$/i,
{
teamId: {},
})
it('sub segments (params + static)', () => {
- const pattern = new MatcherPatternPathCustomParams(
+ const pattern = new MatcherPatternPathDynamic(
/^\/teams\/([^/]+?)-b-([^/]+?)$/i,
{
teamId: {},
import { describe, expectTypeOf, it } from 'vitest'
-import {
- MatcherPatternPathCustomParams,
- PARAM_INTEGER_SINGLE,
- PATH_PARAM_DEFAULT_PARSER,
- PATH_PARAM_SINGLE_DEFAULT,
-} from './matcher-pattern'
+import { MatcherPatternPathDynamic } from './matcher-pattern'
+import { PARAM_INTEGER_SINGLE } from './param-parsers/numbers'
+import { PATH_PARAM_DEFAULT_PARSER } from './param-parsers'
+import { PATH_PARAM_SINGLE_DEFAULT } from './param-parsers'
describe('MatcherPatternPathCustomParams', () => {
it('can be generic', () => {
- const matcher = new MatcherPatternPathCustomParams(
+ const matcher = new MatcherPatternPathDynamic(
/^\/users\/([^/]+)$/i,
{ userId: { ...PATH_PARAM_DEFAULT_PARSER } },
['users', 0]
})
it('can be a simple param', () => {
- const matcher = new MatcherPatternPathCustomParams(
+ const matcher = new MatcherPatternPathDynamic(
/^\/users\/([^/]+)\/([^/]+)$/i,
{ userId: { ...PATH_PARAM_SINGLE_DEFAULT, repeat: true } },
['users', 0]
})
it('can be a custom type', () => {
- const matcher = new MatcherPatternPathCustomParams(
+ const matcher = new MatcherPatternPathDynamic(
/^\/profiles\/([^/]+)$/i,
{
userId: {
import { warn } from '../../../warning'
import { decode, MatcherQueryParams } from '../resolver-abstract'
import { miss } from './errors'
+import { ParamParser } from './param-parsers/types'
/**
* Base interface for matcher patterns that extract params from a URL.
}
/**
- * Allows matching a static path folllowed by anything.
- *
- * @example
- *
- * ```ts
- * const matcher = new MatcherPatternPathStar('/team')
- * matcher.match('/team/123') // { pathMatch: '/123' }
- * matcher.match('/team/123/more') // { pathMatch: '/123/more' }
- * matcher.match('/team-123') // { pathMatch: '-123' }
- * matcher.match('/team') // { pathMatch: '' }
- * matcher.build({ pathMatch: '/123' }) // '/team/123'
- * ```
+ * Options for param parsers in {@link MatcherPatternPathDynamic}.
*/
-export class MatcherPatternPathStar
- implements MatcherPatternPath<{ pathMatch: string }>
-{
- private path: string
- constructor(path: string = '') {
- this.path = path.toLowerCase()
- }
-
- match(path: string): { pathMatch: string } {
- if (!path.toLowerCase().startsWith(this.path)) {
- throw miss()
- }
- return {
- pathMatch: path.slice(this.path.length),
- }
- }
-
- build(params: { pathMatch: string }): string {
- return this.path + params.pathMatch
- }
-}
-
-// example of a static matcher built at runtime
-// new MatcherPatternPathStatic('/')
-// new MatcherPatternPathStatic('/team')
-
-export interface ParamParser<
- TOut = string | string[] | null,
- TIn extends string | string[] | null = string | string[] | null,
-> {
- get?: (value: NoInfer<TIn>) => TOut
- set?: (value: NoInfer<TOut>) => TIn
-}
-
-export type ParamParser_Generic =
- | ParamParser<any, string>
- | ParamParser<any, string[]>
-// TODO: these are possible values for optional params
-// | null | undefined
-
-/**
- * Type safe helper to define a param parser.
- *
- * @param parser - the parser to define. Will be returned as is.
- */
-/*! #__NO_SIDE_EFFECTS__ */
-export function defineParamParser<TOut, TIn extends string | string[]>(parser: {
- get?: (value: TIn) => TOut
- set?: (value: TOut) => TIn
-}): ParamParser<TOut, TIn> {
- return parser
-}
-
-const PATH_PARAM_DEFAULT_GET = (value: string | string[] | null | undefined) =>
- value ?? null
-export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {}
-
-const PATH_PARAM_DEFAULT_SET = (value: string | string[] | null | undefined) =>
- value && Array.isArray(value) ? value.map(String) : String(value)
-// TODO: `(value an null | undefined)` for types
-export const PATH_PARAM_DEFAULT_PARSER: ParamParser = {
- get: PATH_PARAM_DEFAULT_GET,
- set: PATH_PARAM_DEFAULT_SET,
-}
-
-/**
- * NOTE: I tried to make this generic and infer the types from the params but failed. This is what I tried:
- * ```ts
- * export type ParamsFromParsers<P extends Record<string, ParamParser_Generic>> = {
- * [K in keyof P]: P[K] extends Param_GetSet<infer TIn, infer TOut>
- * ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[]
- * ? TIn
- * : TOut
- * : never
- * }
- *
- * export class MatcherPatternPathDynamic<
- * ParamsParser extends Record<string, ParamParser_Generic>
- * > implements MatcherPatternPath<ParamsFromParsers<ParamsParser>>
- * {
- * private params: Record<string, Required<ParamParser_Generic>> = {}
- * constructor(
- * private re: RegExp,
- * params: ParamsParser,
- * public build: (params: ParamsFromParsers<ParamsParser>) => string
- * ) {}
- * ```
- * It ended up not working in one place or another. It could probably be fixed by
- */
-
-export type ParamsFromParsers<P extends Record<string, ParamParser_Generic>> = {
- [K in keyof P]: P[K] extends ParamParser<infer TOut, infer TIn>
- ? unknown extends TOut // if any or unknown, use the value of TIn, which defaults to string | string[]
- ? TIn
- : TOut
- : never
-}
-
-interface MatcherPatternPathCustomParamOptions<
+export interface MatcherPatternPathDynamic_ParamOptions<
TIn extends string | string[] | null = string | string[] | null,
TOut = string | string[] | null,
> extends ParamParser<TOut, TIn> {
* @internal
*/
type ExtractParamTypeFromOptions<TParamsOptions> = {
- [K in keyof TParamsOptions]: TParamsOptions[K] extends MatcherPatternPathCustomParamOptions<
+ [K in keyof TParamsOptions]: TParamsOptions[K] extends MatcherPatternPathDynamic_ParamOptions<
any,
infer TOut
>
: never
}
-const IS_INTEGER_RE = /^-?\d+$/
-
-export const PARAM_INTEGER_SINGLE = {
- get: (value: string) => {
- if (IS_INTEGER_RE.test(value)) {
- const num = Number(value)
- if (Number.isFinite(num)) {
- return num
- }
- }
- throw miss()
- },
- set: (value: number) => String(value),
-} satisfies ParamParser<number, string>
-
-export const PARAM_NUMBER_OPTIONAL = {
- get: (value: string | null) =>
- value == null ? null : PARAM_INTEGER_SINGLE.get(value),
- set: (value: number | null) =>
- value != null ? PARAM_INTEGER_SINGLE.set(value) : null,
-} satisfies ParamParser<number | null, string | null>
-
-export const PARAM_NUMBER_REPEATABLE = {
- get: (value: string[]) => value.map(PARAM_INTEGER_SINGLE.get),
- set: (value: number[]) => value.map(PARAM_INTEGER_SINGLE.set),
-} satisfies ParamParser<number[], string[]>
-
-export const PARAM_NUMBER_REPEATABLE_OPTIONAL = {
- get: (value: string[] | null) =>
- value == null ? null : PARAM_NUMBER_REPEATABLE.get(value),
- set: (value: number[] | null) =>
- value != null ? PARAM_NUMBER_REPEATABLE.set(value) : null,
-} satisfies ParamParser<number[] | null, string[] | null>
-
/**
- * Native Param parser for integers.
- *
- * @internal
+ * Handles the `path` part of a URL with dynamic parameters.
*/
-export const PARAM_PARSER_INT: ParamParser<number | number[] | null> = {
- get: value =>
- Array.isArray(value)
- ? PARAM_NUMBER_REPEATABLE.get(value)
- : value != null
- ? PARAM_INTEGER_SINGLE.get(value)
- : null,
- set: value =>
- Array.isArray(value)
- ? PARAM_NUMBER_REPEATABLE.set(value)
- : value != null
- ? PARAM_INTEGER_SINGLE.set(value)
- : null,
-}
-
-export class MatcherPatternPathCustomParams<
+export class MatcherPatternPathDynamic<
TParamsOptions,
// TODO: | EmptyObject ?
// TParamsOptions extends Record<string, MatcherPatternPathCustomParamOptions>,
// to properly infer the types of the params when using `new MatcherPatternPathCustomParams()`
// otherwise, we need to use a factory function: https://github.com/microsoft/TypeScript/issues/40451
readonly params: TParamsOptions &
- Record<string, MatcherPatternPathCustomParamOptions<any, any>>,
+ Record<string, MatcherPatternPathDynamic_ParamOptions<any, any>>,
// A better version could be using all the parts to join them
// .e.g ['users', 0, 'profile', 1] -> /users/123/profile/456
// numbers are indexes of the params in the params object keys
}
}
-/**
- * Matcher for dynamic paths, e.g. `/team/:id/:name`.
- * Supports one, one or zero, one or more and zero or more params.
- */
-export class MatcherPatternPathDynamic<
- TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
-> implements MatcherPatternPath<TParams>
-{
- private params: Record<string, Required<ParamParser_Generic>> = {}
-
- constructor(
- private re: RegExp,
- params: Record<keyof TParams, ParamParser_Generic>,
- public build: (params: TParams) => string,
- private opts: { repeat?: boolean; optional?: boolean } = {}
- ) {
- for (const paramName in params) {
- const param = params[paramName]
- this.params[paramName] = {
- get: param.get || PATH_PARAM_DEFAULT_GET,
- // @ts-expect-error FIXME: should work
- set: param.set || PATH_PARAM_DEFAULT_SET,
- }
- }
- }
-
- /**
- * Match path against the pattern and return
- *
- * @param path - path to match
- * @throws if the patch doesn't match
- * @returns matched decoded params
- */
- match(path: string): TParams {
- const match = path.match(this.re)
- if (!match) {
- throw miss()
- }
- let i = 1 // index in match array
- const params = {} as TParams
- for (const paramName in this.params) {
- const currentParam = this.params[paramName]
- const currentMatch = match[i++]
- let value: string | null | string[] =
- this.opts.optional && currentMatch == null ? null : currentMatch
- value = this.opts.repeat && value ? value.split('/') : value
-
- params[paramName as keyof typeof params] = currentParam.get(
- // @ts-expect-error: FIXME: the type of currentParam['get'] is wrong
- value && (Array.isArray(value) ? value.map(decode) : decode(value))
- ) as (typeof params)[keyof typeof params]
- }
-
- if (__DEV__ && i !== match.length) {
- warn(
- `Regexp matched ${match.length} params, but ${i} params are defined. Found when matching "${path}" against ${String(this.re)}`
- )
- }
- return params
- }
-
- // build(params: TParams): string {
- // let path = this.re.source
- // for (const param of this.params) {
- // const value = params[param.name as keyof TParams]
- // if (value == null) {
- // throw new Error(`Matcher build: missing param ${param.name}`)
- // }
- // path = path.replace(
- // /([^\\]|^)\([^?]*\)/,
- // `$1${encodeParam(param.set(value))}`
- // )
- // }
- // return path
- // }
-}
-
/**
* Handles the `query` part of a URL. It can transform a query object into an
* object of params and vice versa.
--- /dev/null
+import type { ParamParser } from './types'
+
+// TODO: these are possible values for optional params
+// | null | undefined
+/**
+ * Type safe helper to define a param parser.
+ *
+ * @param parser - the parser to define. Will be returned as is.
+ */
+/*! #__NO_SIDE_EFFECTS__ */
+
+export function defineParamParser<TOut, TIn extends string | string[]>(parser: {
+ get?: (value: TIn) => TOut
+ set?: (value: TOut) => TIn
+}): ParamParser<TOut, TIn> {
+ return parser
+}
+export const PATH_PARAM_DEFAULT_GET = (
+ value: string | string[] | null | undefined
+) => value ?? null
+export const PATH_PARAM_SINGLE_DEFAULT: ParamParser<string, string> = {}
+export const PATH_PARAM_DEFAULT_SET = (
+ value: string | string[] | null | undefined
+) => (value && Array.isArray(value) ? value.map(String) : String(value)) // TODO: `(value an null | undefined)` for types
+
+export const PATH_PARAM_DEFAULT_PARSER: ParamParser = {
+ get: PATH_PARAM_DEFAULT_GET,
+ set: PATH_PARAM_DEFAULT_SET,
+}
+
+export { ParamParser }
+
+export { PARAM_PARSER_INT } from './numbers'
--- /dev/null
+import { miss } from '../errors'
+import { ParamParser } from './types'
+
+export const PARAM_INTEGER_SINGLE = {
+ get: (value: string) => {
+ if (IS_INTEGER_RE.test(value)) {
+ const num = Number(value)
+ if (Number.isFinite(num)) {
+ return num
+ }
+ }
+ throw miss()
+ },
+ set: (value: number) => String(value),
+} satisfies ParamParser<number, string>
+export const IS_INTEGER_RE = /^-?\d+$/
+export const PARAM_NUMBER_OPTIONAL = {
+ get: (value: string | null) =>
+ value == null ? null : PARAM_INTEGER_SINGLE.get(value),
+ set: (value: number | null) =>
+ value != null ? PARAM_INTEGER_SINGLE.set(value) : null,
+} satisfies ParamParser<number | null, string | null>
+export const PARAM_NUMBER_REPEATABLE = {
+ get: (value: string[]) => value.map(PARAM_INTEGER_SINGLE.get),
+ set: (value: number[]) => value.map(PARAM_INTEGER_SINGLE.set),
+} satisfies ParamParser<number[], string[]>
+export const PARAM_NUMBER_REPEATABLE_OPTIONAL = {
+ get: (value: string[] | null) =>
+ value == null ? null : PARAM_NUMBER_REPEATABLE.get(value),
+ set: (value: number[] | null) =>
+ value != null ? PARAM_NUMBER_REPEATABLE.set(value) : null,
+} satisfies ParamParser<number[] | null, string[] | null> /**
+ * Native Param parser for integers.
+ *
+ * @internal
+ */
+
+export const PARAM_PARSER_INT: ParamParser<number | number[] | null> = {
+ get: value =>
+ Array.isArray(value)
+ ? PARAM_NUMBER_REPEATABLE.get(value)
+ : value != null
+ ? PARAM_INTEGER_SINGLE.get(value)
+ : null,
+ set: value =>
+ Array.isArray(value)
+ ? PARAM_NUMBER_REPEATABLE.set(value)
+ : value != null
+ ? PARAM_INTEGER_SINGLE.set(value)
+ : null,
+}
--- /dev/null
+/**
+ * Defines a parser that can read a param from the url (string-based) and
+ * transform it into a more complex type, or vice versa.
+ *
+ * @see MatcherPattern
+ */
+export interface ParamParser<
+ TOut = string | string[] | null,
+ TIn extends string | string[] | null = string | string[] | null,
+> {
+ get?: (value: NoInfer<TIn>) => TOut
+ set?: (value: NoInfer<TOut>) => TIn
+}
+
+/**
+ * Generic type for a param parser that can handle both single and repeatable params.
+ *
+ * @see ParamParser
+ */
+export type ParamParser_Generic =
+ | ParamParser<any, string>
+ | ParamParser<any, string[]>
+++ /dev/null
-import { describe, expect, it } from 'vitest'
-import { NO_MATCH_LOCATION, pathEncoded } from '../resolver-abstract'
-import { createCompiledMatcher } from './resolver-dynamic'
-import {
- MatcherPatternQuery,
- MatcherPatternPathStatic,
- MatcherPatternPathDynamic,
-} from '../matchers/matcher-pattern'
-import {
- EMPTY_PATH_ROUTE,
- USER_ID_ROUTE,
- ANY_PATH_ROUTE,
- ANY_PATH_PATTERN_MATCHER,
- EMPTY_PATH_PATTERN_MATCHER,
- USER_ID_PATH_PATTERN_MATCHER,
- ANY_HASH_PATTERN_MATCHER,
-} from '../matchers/test-utils'
-
-const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = {
- match: query => {
- const page = Number(query.page)
- return {
- page: Number.isNaN(page) ? 1 : page,
- }
- },
- build: params => ({ page: String(params.page) }),
-} satisfies MatcherPatternQuery<{ page: number }>
-
-describe('RouterMatcher', () => {
- describe('new matchers', () => {
- it('static path', () => {
- const matcher = createCompiledMatcher([
- { path: new MatcherPatternPathStatic('/'), score: [[80]] },
- { path: new MatcherPatternPathStatic('/users'), score: [[80]] },
- ])
-
- expect(matcher.resolve({ path: '/' })).toMatchObject({
- fullPath: '/',
- path: '/',
- params: {},
- query: {},
- hash: '',
- })
-
- expect(matcher.resolve({ path: '/users' })).toMatchObject({
- fullPath: '/users',
- path: '/users',
- params: {},
- query: {},
- hash: '',
- })
- })
-
- it('dynamic path', () => {
- const matcher = createCompiledMatcher([
- {
- score: [[80], [70]],
- path: new MatcherPatternPathDynamic<{ id: string }>(
- /^\/users\/([^\/]+)$/,
- {
- id: {},
- },
- ({ id }) => pathEncoded`/users/${id}`
- ),
- },
- ])
-
- expect(matcher.resolve({ path: '/users/1' })).toMatchObject({
- fullPath: '/users/1',
- path: '/users/1',
- params: { id: '1' },
- })
- })
- })
-
- describe('adding and removing', () => {
- it('add static path', () => {
- const matcher = createCompiledMatcher()
- matcher.addMatcher(EMPTY_PATH_ROUTE)
- })
-
- it('adds dynamic path', () => {
- const matcher = createCompiledMatcher()
- matcher.addMatcher(USER_ID_ROUTE)
- })
-
- it('removes static path', () => {
- const matcher = createCompiledMatcher()
- matcher.addMatcher(EMPTY_PATH_ROUTE)
- matcher.removeMatcher(EMPTY_PATH_ROUTE)
- // Add assertions to verify the route was removed
- })
-
- it('removes dynamic path', () => {
- const matcher = createCompiledMatcher()
- matcher.addMatcher(USER_ID_ROUTE)
- matcher.removeMatcher(USER_ID_ROUTE)
- // Add assertions to verify the route was removed
- })
- })
-
- describe('resolve()', () => {
- describe.todo('absolute locations as strings', () => {
- it('resolves string locations with no params', () => {
- const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE])
-
- expect(matcher.resolve({ path: '/?a=a&b=b#h' })).toMatchObject({
- path: '/',
- params: {},
- query: { a: 'a', b: 'b' },
- hash: '#h',
- })
- })
-
- it('resolves a not found string', () => {
- const matcher = createCompiledMatcher()
- expect(matcher.resolve({ path: '/bar?q=1#hash' })).toEqual({
- ...NO_MATCH_LOCATION,
- fullPath: '/bar?q=1#hash',
- path: '/bar',
- query: { q: '1' },
- hash: '#hash',
- matched: [],
- })
- })
-
- it('resolves string locations with params', () => {
- const matcher = createCompiledMatcher([USER_ID_ROUTE])
-
- expect(matcher.resolve({ path: '/users/1?a=a&b=b#h' })).toMatchObject({
- path: '/users/1',
- params: { id: 1 },
- query: { a: 'a', b: 'b' },
- hash: '#h',
- })
- expect(matcher.resolve({ path: '/users/54?a=a&b=b#h' })).toMatchObject({
- path: '/users/54',
- params: { id: 54 },
- query: { a: 'a', b: 'b' },
- hash: '#h',
- })
- })
-
- it('resolve string locations with query', () => {
- const matcher = createCompiledMatcher([
- {
- path: ANY_PATH_PATTERN_MATCHER,
- score: [[100, -10]],
- query: PAGE_QUERY_PATTERN_MATCHER,
- },
- ])
-
- expect(matcher.resolve({ path: '/foo?page=100&b=b#h' })).toMatchObject({
- params: { page: 100 },
- path: '/foo',
- query: {
- page: '100',
- b: 'b',
- },
- hash: '#h',
- })
- })
-
- it('resolves string locations with hash', () => {
- const matcher = createCompiledMatcher([
- {
- score: [[100, -10]],
- path: ANY_PATH_PATTERN_MATCHER,
- hash: ANY_HASH_PATTERN_MATCHER,
- },
- ])
-
- expect(matcher.resolve({ path: '/foo?a=a&b=b#bar' })).toMatchObject({
- hash: '#bar',
- params: { hash: 'bar' },
- path: '/foo',
- query: { a: 'a', b: 'b' },
- })
- })
-
- it('combines path, query and hash params', () => {
- const matcher = createCompiledMatcher([
- {
- score: [[200, 80], [72]],
- path: USER_ID_PATH_PATTERN_MATCHER,
- query: PAGE_QUERY_PATTERN_MATCHER,
- hash: ANY_HASH_PATTERN_MATCHER,
- },
- ])
-
- expect(
- matcher.resolve({ path: '/users/24?page=100#bar' })
- ).toMatchObject({
- params: { id: 24, page: 100, hash: 'bar' },
- })
- })
- })
-
- describe('relative locations as strings', () => {
- it('resolves a simple relative location', () => {
- const matcher = createCompiledMatcher([
- { path: ANY_PATH_PATTERN_MATCHER, score: [[-10]] },
- ])
-
- expect(
- matcher.resolve(
- { path: 'foo' },
- matcher.resolve({ path: '/nested/' })
- )
- ).toMatchObject({
- params: {},
- path: '/nested/foo',
- query: {},
- hash: '',
- })
- expect(
- matcher.resolve(
- { path: '../foo' },
- matcher.resolve({ path: '/nested/' })
- )
- ).toMatchObject({
- params: {},
- path: '/foo',
- query: {},
- hash: '',
- })
- expect(
- matcher.resolve(
- { path: './foo' },
- matcher.resolve({ path: '/nested/' })
- )
- ).toMatchObject({
- params: {},
- path: '/nested/foo',
- query: {},
- hash: '',
- })
- })
- })
-
- describe('absolute locations as objects', () => {
- it('resolves an object location', () => {
- const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE])
- expect(matcher.resolve({ path: '/' })).toMatchObject({
- fullPath: '/',
- path: '/',
- params: {},
- query: {},
- hash: '',
- })
- })
- })
-
- describe('named locations', () => {
- it('resolves named locations with no params', () => {
- const matcher = createCompiledMatcher([
- {
- name: 'home',
- path: EMPTY_PATH_PATTERN_MATCHER,
- score: [[80]],
- },
- ])
-
- expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({
- name: 'home',
- path: '/',
- params: {},
- query: {},
- hash: '',
- })
- })
- })
-
- describe('encoding', () => {
- const matcher = createCompiledMatcher([ANY_PATH_ROUTE])
- describe('decodes', () => {
- it('handles encoded string path', () => {
- expect(matcher.resolve({ path: '/%23%2F%3F' })).toMatchObject({
- fullPath: '/%23%2F%3F',
- path: '/%23%2F%3F',
- query: {},
- params: {},
- hash: '',
- })
- })
-
- it('decodes query from a string', () => {
- expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({
- path: '/foo',
- fullPath: '/foo?foo=%23%2F%3F',
- query: { foo: '#/?' },
- })
- })
-
- it('decodes hash from a string', () => {
- expect(matcher.resolve('/foo#%22')).toMatchObject({
- path: '/foo',
- fullPath: '/foo#%22',
- hash: '#"',
- })
- })
- })
-
- describe('encodes', () => {
- it('encodes the query', () => {
- expect(
- matcher.resolve({ path: '/foo', query: { foo: '"' } })
- ).toMatchObject({
- fullPath: '/foo?foo=%22',
- query: { foo: '"' },
- })
- })
-
- it('encodes the hash', () => {
- expect(matcher.resolve({ path: '/foo', hash: '#"' })).toMatchObject({
- fullPath: '/foo#%22',
- hash: '#"',
- })
- })
- })
- })
- })
-})