From 62811236644dc37f23ea15f2b8e7de7b1ac89d8d Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Sat, 16 Aug 2025 17:46:58 +0200 Subject: [PATCH] refactor: exports and rename MatcherPatternPathDynamic --- packages/router/src/experimental/index.ts | 17 +- .../matchers/matcher-pattern-path-star.ts | 38 +++ .../matchers/matcher-pattern.spec.ts | 18 +- .../matchers/matcher-pattern.test-d.ts | 16 +- .../matchers/matcher-pattern.ts | 250 +------------- .../matchers/param-parsers/index.ts | 33 ++ .../matchers/param-parsers/numbers.ts | 51 +++ .../matchers/param-parsers/types.ts | 22 ++ .../old/resolver-dynamic.spec.ts | 323 ------------------ 9 files changed, 177 insertions(+), 591 deletions(-) create mode 100644 packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts create mode 100644 packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts create mode 100644 packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts create mode 100644 packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts delete mode 100644 packages/router/src/experimental/route-resolver/old/resolver-dynamic.spec.ts diff --git a/packages/router/src/experimental/index.ts b/packages/router/src/experimental/index.ts index 7e1a0ff6..48a16279 100644 --- a/packages/router/src/experimental/index.ts +++ b/packages/router/src/experimental/index.ts @@ -19,23 +19,26 @@ export type { 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 diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts new file mode 100644 index 00000000..7c28f7bd --- /dev/null +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern-path-star.ts @@ -0,0 +1,38 @@ +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 + } +} diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts index 74a8c3f6..2e4d491f 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts @@ -1,9 +1,9 @@ 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()', () => { @@ -109,7 +109,7 @@ describe('MatcherPatternPathStar', () => { describe('MatcherPatternPathCustom', () => { it('single param', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams\/([^/]+?)\/b$/i, { // all defaults @@ -131,7 +131,7 @@ describe('MatcherPatternPathCustom', () => { }) it('decodes single param', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams\/([^/]+?)$/i, { teamId: {}, @@ -143,7 +143,7 @@ describe('MatcherPatternPathCustom', () => { }) it('optional param', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams(?:\/([^/]+?))?\/b$/i, { teamId: {}, @@ -160,7 +160,7 @@ describe('MatcherPatternPathCustom', () => { }) it('repeatable param', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams\/(.+?)\/b$/i, { teamId: { repeat: true }, @@ -179,7 +179,7 @@ describe('MatcherPatternPathCustom', () => { }) it('repeatable optional param', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams(?:\/(.+?))?\/b$/i, { teamId: { repeat: true }, @@ -202,7 +202,7 @@ describe('MatcherPatternPathCustom', () => { }) it('multiple params', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams\/([^/]+?)\/([^/]+?)$/i, { teamId: {}, @@ -224,7 +224,7 @@ describe('MatcherPatternPathCustom', () => { }) it('sub segments (params + static)', () => { - const pattern = new MatcherPatternPathCustomParams( + const pattern = new MatcherPatternPathDynamic( /^\/teams\/([^/]+?)-b-([^/]+?)$/i, { teamId: {}, diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts index d436153f..de39f167 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.test-d.ts @@ -1,14 +1,12 @@ 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] @@ -33,7 +31,7 @@ describe('MatcherPatternPathCustomParams', () => { }) 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] @@ -51,7 +49,7 @@ describe('MatcherPatternPathCustomParams', () => { }) it('can be a custom type', () => { - const matcher = new MatcherPatternPathCustomParams( + const matcher = new MatcherPatternPathDynamic( /^\/profiles\/([^/]+)$/i, { userId: { diff --git a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts index 901d7509..73fe26ec 100644 --- a/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts @@ -3,6 +3,7 @@ import { encodeParam } from '../../../encoding' 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. @@ -86,118 +87,9 @@ export class MatcherPatternPathStatic } /** - * 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) => TOut - set?: (value: NoInfer) => TIn -} - -export type ParamParser_Generic = - | ParamParser - | ParamParser -// 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(parser: { - get?: (value: TIn) => TOut - set?: (value: TOut) => TIn -}): ParamParser { - return parser -} - -const PATH_PARAM_DEFAULT_GET = (value: string | string[] | null | undefined) => - value ?? null -export const PATH_PARAM_SINGLE_DEFAULT: ParamParser = {} - -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

> = { - * [K in keyof P]: P[K] extends Param_GetSet - * ? 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 - * > implements MatcherPatternPath> - * { - * private params: Record> = {} - * constructor( - * private re: RegExp, - * params: ParamsParser, - * public build: (params: ParamsFromParsers) => string - * ) {} - * ``` - * It ended up not working in one place or another. It could probably be fixed by - */ - -export type ParamsFromParsers

> = { - [K in keyof P]: P[K] extends ParamParser - ? 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 { @@ -212,7 +104,7 @@ interface MatcherPatternPathCustomParamOptions< * @internal */ type ExtractParamTypeFromOptions = { - [K in keyof TParamsOptions]: TParamsOptions[K] extends MatcherPatternPathCustomParamOptions< + [K in keyof TParamsOptions]: TParamsOptions[K] extends MatcherPatternPathDynamic_ParamOptions< any, infer TOut > @@ -220,61 +112,10 @@ type ExtractParamTypeFromOptions = { : 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 - -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 - -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 - -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 - /** - * Native Param parser for integers. - * - * @internal + * Handles the `path` part of a URL with dynamic parameters. */ -export const PARAM_PARSER_INT: ParamParser = { - 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, @@ -292,7 +133,7 @@ export class MatcherPatternPathCustomParams< // 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>, + Record>, // 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 @@ -376,83 +217,6 @@ export class MatcherPatternPathCustomParams< } } -/** - * 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 -{ - private params: Record> = {} - - constructor( - private re: RegExp, - params: Record, - 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. diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts new file mode 100644 index 00000000..166a87c3 --- /dev/null +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/index.ts @@ -0,0 +1,33 @@ +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(parser: { + get?: (value: TIn) => TOut + set?: (value: TOut) => TIn +}): ParamParser { + return parser +} +export const PATH_PARAM_DEFAULT_GET = ( + value: string | string[] | null | undefined +) => value ?? null +export const PATH_PARAM_SINGLE_DEFAULT: ParamParser = {} +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' diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts new file mode 100644 index 00000000..dabd9526 --- /dev/null +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/numbers.ts @@ -0,0 +1,51 @@ +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 +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 +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 +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 /** + * Native Param parser for integers. + * + * @internal + */ + +export const PARAM_PARSER_INT: ParamParser = { + 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, +} diff --git a/packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts b/packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts new file mode 100644 index 00000000..c4b65d27 --- /dev/null +++ b/packages/router/src/experimental/route-resolver/matchers/param-parsers/types.ts @@ -0,0 +1,22 @@ +/** + * 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) => TOut + set?: (value: NoInfer) => TIn +} + +/** + * Generic type for a param parser that can handle both single and repeatable params. + * + * @see ParamParser + */ +export type ParamParser_Generic = + | ParamParser + | ParamParser diff --git a/packages/router/src/experimental/route-resolver/old/resolver-dynamic.spec.ts b/packages/router/src/experimental/route-resolver/old/resolver-dynamic.spec.ts deleted file mode 100644 index a1ebbde6..00000000 --- a/packages/router/src/experimental/route-resolver/old/resolver-dynamic.spec.ts +++ /dev/null @@ -1,323 +0,0 @@ -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: '#"', - }) - }) - }) - }) - }) -}) -- 2.47.3