*/
export type MatcherParamsFormatted = Record<string, unknown>
-export interface MatcherLocationAsName {
+export interface MatcherLocationAsNamed {
name: MatcherName
params: MatcherParamsFormatted
query?: LocationQueryRaw
* query: { used: String }, // we require a `used` query param
* })
* // /?used=2
- * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null
+ * pattern.parseLocation({ path: '/', query: { used: '' }, hash: '' }) // null becauso no /foo
* // /foo?used=2¬Used¬Used=2#hello
* pattern.parseLocation({ path: '/foo', query: { used: '2', notUsed: [null, '2']}, hash: '#hello' })
- * // { used: '2' } // we extract the required params
- * // /foo?used=2#hello
+ * // [{}, { used: '2' }, {}]// we extract the required params
+ * // /foo?other=2#hello
* pattern.parseLocation({ path: '/foo', query: {}, hash: '#hello' })
* // null // the query param is missing
* ```
export interface PatternQueryParamOptions<T = unknown>
extends PatternParamOptions_Base<T> {
+ // FIXME: can be removed? seems to be the same as above
get: (value: MatcherQueryParamsValue) => T
set?: (value: T) => MatcherQueryParamsValue
}
query: MatcherQueryParams
hash: string
}) {
- // TODO: is this performant? bench compare to a check with `null
+ // TODO: is this performant? bench compare to a check with `null`
try {
return [
this.path.match(location.path),
-import { describe, it } from 'vitest'
+import { describe, expectTypeOf, it } from 'vitest'
import { NEW_LocationResolved, createCompiledMatcher } from './matcher'
describe('Matcher', () => {
- it('resolves locations', () => {
- const matcher = createCompiledMatcher()
- matcher.resolve('/foo')
- // @ts-expect-error: needs currentLocation
- matcher.resolve('foo')
- matcher.resolve('foo', {} as NEW_LocationResolved)
- matcher.resolve({ name: 'foo', params: {} })
- // @ts-expect-error: needs currentLocation
- matcher.resolve({ params: { id: 1 } })
- matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved)
+ const matcher = createCompiledMatcher()
+
+ describe('matcher.resolve()', () => {
+ it('resolves absolute string locations', () => {
+ expectTypeOf(
+ matcher.resolve('/foo')
+ ).toEqualTypeOf<NEW_LocationResolved>()
+ })
+
+ it('fails on non absolute location without a currentLocation', () => {
+ // @ts-expect-error: needs currentLocation
+ matcher.resolve('foo')
+ })
+
+ it('resolves relative locations', () => {
+ expectTypeOf(
+ matcher.resolve('foo', {} as NEW_LocationResolved)
+ ).toEqualTypeOf<NEW_LocationResolved>()
+ })
+
+ it('resolved named locations', () => {
+ expectTypeOf(
+ matcher.resolve({ name: 'foo', params: {} })
+ ).toEqualTypeOf<NEW_LocationResolved>()
+ })
+
+ it('fails on object relative location without a currentLocation', () => {
+ // @ts-expect-error: needs currentLocation
+ matcher.resolve({ params: { id: 1 } })
+ })
+
+ it('resolves object relative locations with a currentLocation', () => {
+ expectTypeOf(
+ matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved)
+ ).toEqualTypeOf<NEW_LocationResolved>()
+ })
})
})
} from '../encoding'
import { parseURL, stringifyURL } from '../location'
import type {
- MatcherLocationAsName,
+ MatcherLocationAsNamed,
MatcherLocationAsRelative,
MatcherParamsFormatted,
} from './matcher-location'
/**
* Resolves a string location relative to another location. A relative location can be `./same-folder`,
- * `../parent-folder`, or even `same-folder`.
+ * `../parent-folder`, `same-folder`, or even `?page=2`.
*/
resolve(
relativeLocation: string,
/**
* Resolves a location by its name. Any required params or query must be passed in the `options` argument.
*/
- resolve(location: MatcherLocationAsName): NEW_LocationResolved
+ resolve(location: MatcherLocationAsNamed): NEW_LocationResolved
/**
* Resolves a location by its path. Any required query must be passed.
type MatcherResolveArgs =
| [absoluteLocation: `/${string}`]
| [relativeLocation: string, currentLocation: NEW_LocationResolved]
- | [location: MatcherLocationAsName]
+ | [location: MatcherLocationAsNamed]
| [
relativeLocation: MatcherLocationAsRelative,
currentLocation: NEW_LocationResolved
export type MatcherQueryParamsValue = string | null | Array<string | null>
export type MatcherQueryParams = Record<string, MatcherQueryParamsValue>
-export function applyToParams<R>(
+/**
+ * Apply a function to all properties in an object. It's used to encode/decode params and queries.
+ * @internal
+ */
+export function applyFnToObject<R>(
fn: (v: string | number | null | undefined) => R,
params: MatcherPathParams | LocationQuery | undefined
): Record<string, R | R[]> {
}
export const NO_MATCH_LOCATION = {
- name: Symbol('no-match'),
+ name: __DEV__ ? Symbol('no-match') : Symbol(),
params: {},
matched: [],
} satisfies Omit<NEW_LocationResolved, 'path' | 'hash' | 'query' | 'fullPath'>
function resolve(...args: MatcherResolveArgs): NEW_LocationResolved {
const [location, currentLocation] = args
+
+ // string location, e.g. '/foo', '../bar', 'baz', '?page=1'
if (typeof location === 'string') {
- // string location, e.g. '/foo', '../bar', 'baz'
const url = parseURL(parseQuery, location, currentLocation?.path)
let matcher: MatcherPattern | undefined
}
} else {
// relative location or by name
+ if (__DEV__ && location.name == null && currentLocation == null) {
+ console.warn(
+ `Cannot resolve an unnamed relative location without a current location. This will throw in production.`,
+ location
+ )
+ return {
+ ...NO_MATCH_LOCATION,
+ fullPath: '/',
+ path: '/',
+ query: {},
+ hash: '',
+ }
+ }
+
+ // either one of them must be defined and is catched by the dev only warn above
const name = location.name ?? currentLocation!.name
const matcher = matchers.get(name)
if (!matcher) {
}
// unencoded params in a formatted form that the user came up with
- const params = location.params ?? currentLocation!.params
+ const params: MatcherParamsFormatted =
+ location.params ?? currentLocation!.params
const mixedUnencodedParams = matcher.matchParams(params)
if (!mixedUnencodedParams) {