import {
nextTick,
shallowReactive,
+ ShallowRef,
shallowRef,
unref,
warn,
type App,
- type Ref,
} from 'vue'
import { RouterLink } from '../RouterLink'
import { RouterView } from '../RouterView'
type RouterHistory,
} from '../history/common'
import type { PathParserOptions } from '../matcher'
-import type { RouteResolver } from '../new-route-resolver/matcher'
+import type {
+ NEW_LocationResolved,
+ NEW_MatcherRecord,
+ NEW_MatcherRecordRaw,
+ NEW_RouterMatcher,
+} from '../new-route-resolver/matcher'
import {
- LocationQuery,
- normalizeQuery,
parseQuery as originalParseQuery,
stringifyQuery as originalStringifyQuery,
} from '../query'
RouteLocationAsRelative,
RouteLocationAsRelativeTyped,
RouteLocationAsString,
+ RouteLocationGeneric,
RouteLocationNormalized,
RouteLocationNormalizedLoaded,
RouteLocationRaw,
isRouteLocation,
isRouteName,
Lazy,
- MatcherLocationRaw,
RouteLocationOptions,
- type RouteRecordRaw,
+ RouteMeta,
} from '../types'
import { useCallbacks } from '../utils/callbacks'
import {
isSameRouteLocation,
parseURL,
START_LOCATION_NORMALIZED,
- stringifyURL,
} from '../location'
import { applyToParams, assign, isArray, isBrowser, noop } from '../utils'
-import { decode, encodeHash, encodeParam } from '../encoding'
+import { decode, encodeParam } from '../encoding'
import {
extractChangingRecords,
extractComponentsGuards,
* Options to initialize an experimental {@link EXPERIMENTAL_Router} instance.
* @experimental
*/
-export interface EXPERIMENTAL_RouterOptions<TRouteRecordRaw, TRouteRecord>
- extends EXPERIMENTAL_RouterOptions_Base {
+export interface EXPERIMENTAL_RouterOptions<
+ TMatcherRecord extends NEW_MatcherRecord
+> extends EXPERIMENTAL_RouterOptions_Base {
/**
* Initial list of routes that should be added to the router.
*/
- routes?: Readonly<RouteRecordRaw[]>
+ routes?: Readonly<EXPERIMENTAL_RouteRecordRaw[]>
/**
* Matcher to use to resolve routes.
* @experimental
*/
- matcher: RouteResolver<TRouteRecordRaw, TRouteRecord>
+ matcher: NEW_RouterMatcher<NEW_MatcherRecordRaw, TMatcherRecord>
}
/**
/**
* Current {@link RouteLocationNormalized}
*/
- readonly currentRoute: Ref<RouteLocationNormalizedLoaded>
+ readonly currentRoute: ShallowRef<RouteLocationNormalizedLoaded>
/**
* Allows turning off the listening of history events. This is a low level api for micro-frontend.
listening: boolean
/**
- * Add a new {@link RouteRecordRaw | route record} as the child of an existing route.
+ * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} as the child of an existing route.
*
* @param parentName - Parent Route Record where `route` should be appended at
* @param route - Route Record to add
addRoute(
// NOTE: it could be `keyof RouteMap` but the point of dynamic routes is not knowing the routes at build
parentName: NonNullable<RouteRecordNameGeneric>,
- route: RouteRecordRaw
+ route: TRouteRecordRaw
): () => void
/**
- * Add a new {@link RouteRecordRaw | route record} to the router.
+ * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router.
*
* @param route - Route Record to add
*/
install(app: App): void
}
-export interface EXPERIMENTAL_Router<TRouteRecordRaw, TRouteRecord>
- extends EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
+export interface EXPERIMENTAL_Router<
+ TRouteRecordRaw, // extends NEW_MatcherRecordRaw,
+ TRouteRecord extends NEW_MatcherRecord
+> extends EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
/**
* Original options object passed to create the Router
*/
- readonly options: EXPERIMENTAL_RouterOptions<TRouteRecordRaw, TRouteRecord>
+ readonly options: EXPERIMENTAL_RouterOptions<TRouteRecord>
+}
+
+export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw {
+ /**
+ * Arbitrary data attached to the record.
+ */
+ meta?: RouteMeta
+}
+
+// TODO: is it worth to have 2 types for the undefined values?
+export interface EXPERIMENTAL_RouteRecordNormalized extends NEW_MatcherRecord {
+ meta: RouteMeta
}
-interface EXPERIMENTAL_RouteRecordRaw {}
-interface EXPERIMENTAL_RouteRecord {}
+function normalizeRouteRecord(
+ record: EXPERIMENTAL_RouteRecordRaw
+): EXPERIMENTAL_RouteRecordNormalized {
+ // FIXME: implementation
+ return {
+ name: __DEV__ ? Symbol('anonymous route record') : Symbol(),
+ meta: {},
+ ...record,
+ }
+}
export function experimental_createRouter(
- options: EXPERIMENTAL_RouterOptions<
- EXPERIMENTAL_RouteRecordRaw,
- EXPERIMENTAL_RouteRecord
- >
-): EXPERIMENTAL_Router<EXPERIMENTAL_RouteRecordRaw, EXPERIMENTAL_RouteRecord> {
+ options: EXPERIMENTAL_RouterOptions<EXPERIMENTAL_RouteRecordNormalized>
+): EXPERIMENTAL_Router<
+ EXPERIMENTAL_RouteRecordRaw,
+ EXPERIMENTAL_RouteRecordNormalized
+> {
const {
matcher,
parseQuery = originalParseQuery,
applyToParams.bind(null, decode)
function addRoute(
- parentOrRoute: NonNullable<RouteRecordNameGeneric> | RouteRecordRaw,
- route?: RouteRecordRaw
+ parentOrRoute:
+ | NonNullable<RouteRecordNameGeneric>
+ | EXPERIMENTAL_RouteRecordRaw,
+ route?: EXPERIMENTAL_RouteRecordRaw
) {
let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
- let record: RouteRecordRaw
+ let rawRecord: EXPERIMENTAL_RouteRecordRaw
+
if (isRouteName(parentOrRoute)) {
parent = matcher.getMatcher(parentOrRoute)
if (__DEV__ && !parent) {
route
)
}
- record = route!
+ rawRecord = route!
} else {
- record = parentOrRoute
+ rawRecord = parentOrRoute
}
- return matcher.addRoute(record, parent)
+ const addedRecord = matcher.addRoute(
+ normalizeRouteRecord(rawRecord),
+ parent
+ )
+
+ return () => {
+ matcher.removeRoute(addedRecord)
+ }
}
function removeRoute(name: NonNullable<RouteRecordNameGeneric>) {
}
function getRoutes() {
- return matcher.getMatchers().map(routeMatcher => routeMatcher.record)
+ return matcher.getMatchers()
}
function hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean {
// const resolve: Router['resolve'] = (rawLocation: RouteLocationRaw, currentLocation) => {
// const objectLocation = routerLocationAsObject(rawLocation)
// we create a copy to modify it later
- currentLocation = assign({}, currentLocation || currentRoute.value)
- if (typeof rawLocation === 'string') {
- const locationNormalized = parseURL(
- parseQuery,
- rawLocation,
- currentLocation.path
- )
- const matchedRoute = matcher.resolve(
- { path: locationNormalized.path },
- currentLocation
- )
+ // TODO: in the experimental version, allow configuring this
+ currentLocation =
+ currentLocation && assign({}, currentLocation || currentRoute.value)
+ // currentLocation = assign({}, currentLocation || currentRoute.value)
- const href = routerHistory.createHref(locationNormalized.fullPath)
- if (__DEV__) {
- if (href.startsWith('//'))
- warn(
- `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
- )
- else if (!matchedRoute.matched.length) {
- warn(`No match found for location with path "${rawLocation}"`)
- }
+ if (__DEV__) {
+ if (!isRouteLocation(rawLocation)) {
+ warn(
+ `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
+ rawLocation
+ )
+ return resolve({})
}
- // locationNormalized is always a new object
- return assign(locationNormalized, matchedRoute, {
- params: decodeParams(matchedRoute.params),
- hash: decode(locationNormalized.hash),
- redirectedFrom: undefined,
- href,
- })
- }
-
- if (__DEV__ && !isRouteLocation(rawLocation)) {
- warn(
- `router.resolve() was passed an invalid location. This will fail in production.\n- Location:`,
- rawLocation
- )
- return resolve({})
- }
-
- let matcherLocation: MatcherLocationRaw
-
- // path could be relative in object as well
- if (rawLocation.path != null) {
if (
- __DEV__ &&
- 'params' in rawLocation &&
- !('name' in rawLocation) &&
- // @ts-expect-error: the type is never
- Object.keys(rawLocation.params).length
+ typeof rawLocation === 'object' &&
+ rawLocation.hash?.startsWith('#')
) {
warn(
- `Path "${rawLocation.path}" was passed with params but they will be ignored. Use a named route alongside params instead.`
+ `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`
)
}
- matcherLocation = assign({}, rawLocation, {
- path: parseURL(parseQuery, rawLocation.path, currentLocation.path).path,
- })
- } else {
- // remove any nullish param
- const targetParams = assign({}, rawLocation.params)
- for (const key in targetParams) {
- if (targetParams[key] == null) {
- delete targetParams[key]
- }
- }
- // pass encoded values to the matcher, so it can produce encoded path and fullPath
- matcherLocation = assign({}, rawLocation, {
- params: encodeParams(targetParams),
- })
- // current location params are decoded, we need to encode them in case the
- // matcher merges the params
- currentLocation.params = encodeParams(currentLocation.params)
}
- const matchedRoute = matcher.resolve(matcherLocation, currentLocation)
- const hash = rawLocation.hash || ''
-
- if (__DEV__ && hash && !hash.startsWith('#')) {
- warn(
- `A \`hash\` should always start with the character "#". Replace "${hash}" with "#${hash}".`
- )
- }
-
- // the matcher might have merged current location params, so
- // we need to run the decoding again
- matchedRoute.params = normalizeParams(decodeParams(matchedRoute.params))
-
- const fullPath = stringifyURL(
- stringifyQuery,
- assign({}, rawLocation, {
- hash: encodeHash(hash),
- path: matchedRoute.path,
- })
+ // FIXME: is this achieved by matchers?
+ // remove any nullish param
+ // if ('params' in rawLocation) {
+ // const targetParams = assign({}, rawLocation.params)
+ // for (const key in targetParams) {
+ // if (targetParams[key] == null) {
+ // delete targetParams[key]
+ // }
+ // }
+ // rawLocation.params = targetParams
+ // }
+
+ const matchedRoute = matcher.resolve(
+ rawLocation,
+ currentLocation satisfies NEW_LocationResolved<EXPERIMENTAL_RouteRecordNormalized>
)
+ const href = routerHistory.createHref(matchedRoute.fullPath)
- const href = routerHistory.createHref(fullPath)
if (__DEV__) {
if (href.startsWith('//')) {
warn(
`Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
)
- } else if (!matchedRoute.matched.length) {
- warn(
- `No match found for location with path "${
- rawLocation.path != null ? rawLocation.path : rawLocation
- }"`
- )
+ }
+ if (!matchedRoute.matched.length) {
+ warn(`No match found for location with path "${rawLocation}"`)
}
}
- return assign(
- {
- fullPath,
- // keep the hash encoded so fullPath is effectively path + encodedQuery +
- // hash
- hash,
- query:
- // if the user is using a custom query lib like qs, we might have
- // nested objects, so we keep the query as is, meaning it can contain
- // numbers at `$route.query`, but at the point, the user will have to
- // use their own type anyway.
- // https://github.com/vuejs/router/issues/328#issuecomment-649481567
- stringifyQuery === originalStringifyQuery
- ? normalizeQuery(rawLocation.query)
- : ((rawLocation.query || {}) as LocationQuery),
- },
- matchedRoute,
- {
- redirectedFrom: undefined,
- href,
- }
- )
+ // TODO: can this be refactored at the very end
+ // matchedRoute is always a new object
+ return assign(matchedRoute, {
+ redirectedFrom: undefined,
+ href,
+ meta: mergeMetaFields(matchedRoute.matched),
+ })
}
function locationAsObject(
}
function replace(to: RouteLocationRaw) {
- return push(assign(locationAsObject(to), { replace: true }))
+ return pushWithRedirect(to, true)
}
function handleRedirectRecord(to: RouteLocation): RouteLocationRaw | void {
function pushWithRedirect(
to: RouteLocationRaw | RouteLocation,
+ _replace?: boolean,
redirectedFrom?: RouteLocation
): Promise<NavigationFailure | void | undefined> {
const targetLocation: RouteLocation = (pendingLocation = resolve(to))
const from = currentRoute.value
const data: HistoryState | undefined = (to as RouteLocationOptions).state
const force: boolean | undefined = (to as RouteLocationOptions).force
- // to could be a string where `replace` is a function
- const replace = (to as RouteLocationOptions).replace === true
+ const replace = (to as RouteLocationOptions).replace ?? _replace
const shouldRedirect = handleRedirectRecord(targetLocation)
? assign({}, data, shouldRedirect.state)
: data,
force,
- replace,
}),
+ replace,
// keep original redirectedFrom if it exists
redirectedFrom || targetLocation
)
return pushWithRedirect(
// keep options
- assign(
- {
- // preserve an existing replacement but allow the redirect to override it
- replace,
- },
- locationAsObject(failure.to),
- {
- state:
- typeof failure.to === 'object'
- ? assign({}, data, failure.to.state)
- : data,
- force,
- }
- ),
+ assign(locationAsObject(failure.to), {
+ state:
+ typeof failure.to === 'object'
+ ? assign({}, data, failure.to.state)
+ : data,
+ force,
+ }),
+ // preserve an existing replacement but allow the redirect to override it
+ replace,
// preserve the original redirectedFrom if any
redirectedFrom || toLocation
)
function runWithContext<T>(fn: () => T): T {
const app: App | undefined = installedApps.values().next().value
+ // TODO: remove safeguard and bump required minimum version of Vue
// support Vue < 3.3
return app && typeof app.runWithContext === 'function'
? app.runWithContext(fn)
const shouldRedirect = handleRedirectRecord(toLocation)
if (shouldRedirect) {
pushWithRedirect(
- assign(shouldRedirect, { replace: true, force: true }),
+ assign(shouldRedirect, { force: true }),
+ true,
toLocation
).catch(noop)
return
assign(locationAsObject((error as NavigationRedirectError).to), {
force: true,
}),
+ undefined,
toLocation
// avoid an uncaught rejection, let push call triggerError
)
let started: boolean | undefined
const installedApps = new Set<App>()
- const router: Router = {
+ const router: EXPERIMENTAL_Router<
+ EXPERIMENTAL_RouteRecordRaw,
+ EXPERIMENTAL_RouteRecordNormalized
+ > = {
currentRoute,
listening: true,
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)
+ // @ts-expect-error: FIXME: refactor with new types once it's possible
app.config.globalProperties.$router = router
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
})
}
+ // @ts-expect-error: FIXME: refactor with new types once it's possible
app.provide(routerKey, router)
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
// TODO: this probably needs to be updated so it can be used by vue-termui
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
+ // @ts-expect-error: FIXME: refactor with new types once it's possible
addDevtools(app, router, matcher)
}
},
return router
}
+
+/**
+ * Merge meta fields of an array of records
+ *
+ * @param matched - array of matched records
+ */
+function mergeMetaFields(
+ matched: NEW_LocationResolved<EXPERIMENTAL_RouteRecordNormalized>['matched']
+): RouteMeta {
+ return assign({} as RouteMeta, ...matched.map(r => r.meta))
+}
-import { decode, MatcherName, MatcherQueryParams } from './matcher'
+import { decode, MatcherQueryParams } from './matcher'
import { EmptyParams, MatcherParamsFormatted } from './matcher-location'
import { miss } from './matchers/errors'
-export interface MatcherPattern {
- /**
- * Name of the matcher. Unique across all matchers.
- */
- name: MatcherName
-
- path: MatcherPatternPath
- query?: MatcherPatternQuery
- hash?: MatcherPatternHash
-
- parent?: MatcherPattern
-}
-
export interface MatcherPatternParams_Base<
TIn = string,
TOut extends MatcherParamsFormatted = MatcherParamsFormatted
import {
createCompiledMatcher,
MatcherLocationRaw,
- MatcherRecordRaw,
+ NEW_MatcherRecordRaw,
NEW_LocationResolved,
} from './matcher'
import { PathParams, tokensToParser } from '../matcher/pathParserRanker'
function compileRouteRecord(
record: RouteRecordRaw,
parentRecord?: RouteRecordRaw
-): MatcherRecordRaw {
+): NEW_MatcherRecordRaw {
// we adapt the path to ensure they are absolute
// TODO: aliases? they could be handled directly in the path matcher
const path = record.path.startsWith('/')
| `/${string}` = START_LOCATION
) {
const records = (Array.isArray(record) ? record : [record]).map(
- (record): MatcherRecordRaw => compileRouteRecord(record)
+ (record): NEW_MatcherRecordRaw => compileRouteRecord(record)
)
const matcher = createCompiledMatcher()
for (const record of records) {
} from './matcher'
import {
MatcherPatternParams_Base,
- MatcherPattern,
MatcherPatternPath,
MatcherPatternQuery,
MatcherPatternPathStatic,
MatcherPatternPathDynamic,
} from './matcher-pattern'
+import { NEW_MatcherRecord } from './matcher'
import { miss } from './matchers/errors'
import { EmptyParams } from './matcher-location'
const EMPTY_PATH_ROUTE = {
name: 'no params',
path: EMPTY_PATH_PATTERN_MATCHER,
-} satisfies MatcherPattern
+} satisfies NEW_MatcherRecord
+
+const ANY_PATH_ROUTE = {
+ name: 'any path',
+ path: ANY_PATH_PATTERN_MATCHER,
+} satisfies NEW_MatcherRecord
const USER_ID_ROUTE = {
name: 'user-id',
path: USER_ID_PATH_PATTERN_MATCHER,
-} satisfies MatcherPattern
+} satisfies NEW_MatcherRecord
describe('RouterMatcher', () => {
describe('new matchers', () => {
const matcher = createCompiledMatcher()
matcher.addRoute(USER_ID_ROUTE)
})
+
+ it('removes static path', () => {
+ const matcher = createCompiledMatcher()
+ matcher.addRoute(EMPTY_PATH_ROUTE)
+ matcher.removeRoute(EMPTY_PATH_ROUTE)
+ // Add assertions to verify the route was removed
+ })
+
+ it('removes dynamic path', () => {
+ const matcher = createCompiledMatcher()
+ matcher.addRoute(USER_ID_ROUTE)
+ matcher.removeRoute(USER_ID_ROUTE)
+ // Add assertions to verify the route was removed
+ })
})
describe('resolve()', () => {
})
})
})
+
+ describe('encoding', () => {
+ it('handles encoded string path', () => {
+ const matcher = createCompiledMatcher([ANY_PATH_ROUTE])
+ console.log(matcher.resolve('/%23%2F%3F'))
+ expect(matcher.resolve('/%23%2F%3F')).toMatchObject({
+ fullPath: '/%23%2F%3F',
+ path: '/%23%2F%3F',
+ query: {},
+ params: {},
+ hash: '',
+ })
+ })
+
+ it('decodes query from a string', () => {
+ const matcher = createCompiledMatcher([ANY_PATH_ROUTE])
+ 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', () => {
+ const matcher = createCompiledMatcher([ANY_PATH_ROUTE])
+ expect(matcher.resolve('/foo#h-%23%2F%3F')).toMatchObject({
+ path: '/foo',
+ fullPath: '/foo#h-%23%2F%3F',
+ hash: '#h-#/?',
+ })
+ })
+ })
})
})
import { describe, expectTypeOf, it } from 'vitest'
-import { NEW_LocationResolved, RouteResolver } from './matcher'
+import {
+ NEW_LocationResolved,
+ NEW_MatcherRecordRaw,
+ NEW_RouterMatcher,
+} from './matcher'
+import { EXPERIMENTAL_RouteRecordNormalized } from '../experimental/router'
describe('Matcher', () => {
- const matcher: RouteResolver<unknown, unknown> = {} as any
+ type TMatcherRecordRaw = NEW_MatcherRecordRaw
+ type TMatcherRecord = EXPERIMENTAL_RouteRecordNormalized
+
+ const matcher: NEW_RouterMatcher<TMatcherRecordRaw, TMatcherRecord> =
+ {} as any
describe('matcher.resolve()', () => {
it('resolves absolute string locations', () => {
- expectTypeOf(
- matcher.resolve('/foo')
- ).toEqualTypeOf<NEW_LocationResolved>()
+ expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf<
+ NEW_LocationResolved<TMatcherRecord>
+ >()
})
it('fails on non absolute location without a currentLocation', () => {
it('resolves relative locations', () => {
expectTypeOf(
- matcher.resolve('foo', {} as NEW_LocationResolved)
- ).toEqualTypeOf<NEW_LocationResolved>()
+ matcher.resolve('foo', {} as NEW_LocationResolved<TMatcherRecord>)
+ ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
})
it('resolved named locations', () => {
- expectTypeOf(
- matcher.resolve({ name: 'foo', params: {} })
- ).toEqualTypeOf<NEW_LocationResolved>()
+ expectTypeOf(matcher.resolve({ name: 'foo', params: {} })).toEqualTypeOf<
+ NEW_LocationResolved<TMatcherRecord>
+ >()
})
it('fails on object relative location without a currentLocation', () => {
it('resolves object relative locations with a currentLocation', () => {
expectTypeOf(
- matcher.resolve({ params: { id: 1 } }, {} as NEW_LocationResolved)
- ).toEqualTypeOf<NEW_LocationResolved>()
+ matcher.resolve(
+ { params: { id: 1 } },
+ {} as NEW_LocationResolved<TMatcherRecord>
+ )
+ ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
})
})
stringifyQuery,
} from '../query'
import type {
- MatcherPattern,
MatcherPatternHash,
MatcherPatternPath,
MatcherPatternQuery,
MatcherLocationAsRelative,
MatcherParamsFormatted,
} from './matcher-location'
+import { _RouteRecordProps } from '../typed-routes'
/**
* Allowed types for a matcher name.
/**
* Manage and resolve routes. Also handles the encoding, decoding, parsing and serialization of params, query, and hash.
+ * `TMatcherRecordRaw` represents the raw record type passed to {@link addRoute}.
+ * `TMatcherRecord` represents the normalized record type.
*/
-export interface RouteResolver<Matcher, MatcherNormalized> {
+export interface NEW_RouterMatcher<TMatcherRecordRaw, TMatcherRecord> {
/**
* Resolves an absolute location (like `/path/to/somewhere`).
*/
- resolve(absoluteLocation: `/${string}`): NEW_LocationResolved
+ resolve(
+ absoluteLocation: `/${string}`,
+ currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TMatcherRecord>
/**
* Resolves a string location relative to another location. A relative location can be `./same-folder`,
*/
resolve(
relativeLocation: string,
- currentLocation: NEW_LocationResolved
- ): NEW_LocationResolved
+ currentLocation: NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TMatcherRecord>
/**
* Resolves a location by its name. Any required params or query must be passed in the `options` argument.
*/
- resolve(location: MatcherLocationAsNamed): NEW_LocationResolved
+ resolve(
+ location: MatcherLocationAsNamed
+ ): NEW_LocationResolved<TMatcherRecord>
/**
* Resolves a location by its absolute path (starts with `/`). Any required query must be passed.
* @param location - The location to resolve.
*/
- resolve(location: MatcherLocationAsPathAbsolute): NEW_LocationResolved
+ resolve(
+ location: MatcherLocationAsPathAbsolute
+ ): NEW_LocationResolved<TMatcherRecord>
resolve(
location: MatcherLocationAsPathRelative,
- currentLocation: NEW_LocationResolved
- ): NEW_LocationResolved
+ currentLocation: NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TMatcherRecord>
// NOTE: in practice, this overload can cause bugs. It's better to use named locations
*/
resolve(
relativeLocation: MatcherLocationAsRelative,
- currentLocation: NEW_LocationResolved
- ): NEW_LocationResolved
+ currentLocation: NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TMatcherRecord>
- addRoute(matcher: Matcher, parent?: MatcherNormalized): MatcherNormalized
- removeRoute(matcher: MatcherNormalized): void
+ addRoute(matcher: TMatcherRecordRaw, parent?: TMatcherRecord): TMatcherRecord
+ removeRoute(matcher: TMatcherRecord): void
clearRoutes(): void
/**
* Get a list of all matchers.
* Previously named `getRoutes()`
*/
- getMatchers(): MatcherNormalized[]
+ getMatchers(): TMatcherRecord[]
/**
* Get a matcher by its name.
* Previously named `getRecordMatcher()`
*/
- getMatcher(name: MatcherName): MatcherNormalized | undefined
+ getMatcher(name: MatcherName): TMatcherRecord | undefined
}
-type MatcherResolveArgs =
- | [absoluteLocation: `/${string}`]
- | [relativeLocation: string, currentLocation: NEW_LocationResolved]
- | [absoluteLocation: MatcherLocationAsPathAbsolute]
- | [
- relativeLocation: MatcherLocationAsPathRelative,
- currentLocation: NEW_LocationResolved
- ]
- | [location: MatcherLocationAsNamed]
- | [
- relativeLocation: MatcherLocationAsRelative,
- currentLocation: NEW_LocationResolved
- ]
-
/**
- * Allowed location objects to be passed to {@link RouteResolver['resolve']}
+ * Allowed location objects to be passed to {@link NEW_RouterMatcher['resolve']}
*/
export type MatcherLocationRaw =
| `/${string}`
type TODO = any
-export interface NEW_LocationResolved {
- name: MatcherName
- fullPath: string
- path: string
+export interface NEW_LocationResolved<TMatched> {
+ // FIXME: remove `undefined`
+ name: MatcherName | undefined
// TODO: generics?
params: MatcherParamsFormatted
+
+ fullPath: string
+ path: string
query: LocationQuery
hash: string
- matched: TODO[]
+ matched: TMatched[]
}
export type MatcherPathParamsValue = string | null | string[]
// // for ts
// value => (value == null ? null : _encodeQueryKey(value))
+/**
+ * Common properties for a location that couldn't be matched. This ensures
+ * having the same name while having a `path`, `query` and `hash` that change.
+ */
export const NO_MATCH_LOCATION = {
name: __DEV__ ? Symbol('no-match') : Symbol(),
params: {},
matched: [],
-} satisfies Omit<NEW_LocationResolved, 'path' | 'hash' | 'query' | 'fullPath'>
+} satisfies Omit<
+ NEW_LocationResolved<unknown>,
+ 'path' | 'hash' | 'query' | 'fullPath'
+>
// FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc)
-export interface MatcherRecordRaw {
+/**
+ * Experiment new matcher record base type.
+ *
+ * @experimental
+ */
+export interface NEW_MatcherRecordRaw {
+ path: MatcherPatternPath
+ query?: MatcherPatternQuery
+ hash?: MatcherPatternHash
+
+ // NOTE: matchers do not handle `redirect` the redirect option, the router
+ // does. They can still match the correct record but they will let the router
+ // retrigger a whole navigation to the new location.
+
+ // TODO: probably as `aliasOf`. Maybe a different format with the path, query and has matchers?
+ /**
+ * Aliases for the record. Allows defining extra paths that will behave like a
+ * copy of the record. Allows having paths shorthands like `/users/:id` and
+ * `/u/:id`. All `alias` and `path` values must share the same params.
+ */
+ // alias?: string | string[]
+
+ /**
+ * Name for the route record. Must be unique. Will be set to `Symbol()` if
+ * not set.
+ */
name?: MatcherName
- path: MatcherPatternPath
+ /**
+ * Array of nested routes.
+ */
+ children?: NEW_MatcherRecordRaw[]
+}
- query?: MatcherPatternQuery
+/**
+ * Normalized version of a {@link NEW_MatcherRecordRaw} record.
+ */
+export interface NEW_MatcherRecord {
+ /**
+ * Name of the matcher. Unique across all matchers.
+ */
+ name: MatcherName
+ path: MatcherPatternPath
+ query?: MatcherPatternQuery
hash?: MatcherPatternHash
- children?: MatcherRecordRaw[]
+ parent?: NEW_MatcherRecord
}
/**
/**
* Build the `matched` array of a record that includes all parent records from the root to the current one.
*/
-function buildMatched(record: MatcherPattern): MatcherPattern[] {
- const matched: MatcherPattern[] = []
- let node: MatcherPattern | undefined = record
+function buildMatched(record: NEW_MatcherRecord): NEW_MatcherRecord[] {
+ const matched: NEW_MatcherRecord[] = []
+ let node: NEW_MatcherRecord | undefined = record
while (node) {
matched.unshift(node)
node = node.parent
}
export function createCompiledMatcher(
- records: MatcherRecordRaw[] = []
-): RouteResolver<MatcherRecordRaw, MatcherPattern> {
+ records: NEW_MatcherRecordRaw[] = []
+): NEW_RouterMatcher<NEW_MatcherRecordRaw, NEW_MatcherRecord> {
// TODO: we also need an array that has the correct order
- const matchers = new Map<MatcherName, MatcherPattern>()
+ const matchers = new Map<MatcherName, NEW_MatcherRecord>()
// TODO: allow custom encode/decode functions
// const encodeParams = applyToParams.bind(null, encodeParam)
// )
// const decodeQuery = transformObject.bind(null, decode, decode)
- function resolve(...args: MatcherResolveArgs): NEW_LocationResolved {
+ // NOTE: because of the overloads, we need to manually type the arguments
+ type MatcherResolveArgs =
+ | [
+ absoluteLocation: `/${string}`,
+ currentLocation?: undefined | NEW_LocationResolved<NEW_MatcherRecord>
+ ]
+ | [
+ relativeLocation: string,
+ currentLocation: NEW_LocationResolved<NEW_MatcherRecord>
+ ]
+ | [absoluteLocation: MatcherLocationAsPathAbsolute]
+ | [
+ relativeLocation: MatcherLocationAsPathRelative,
+ currentLocation: NEW_LocationResolved<NEW_MatcherRecord>
+ ]
+ | [location: MatcherLocationAsNamed]
+ | [
+ relativeLocation: MatcherLocationAsRelative,
+ currentLocation: NEW_LocationResolved<NEW_MatcherRecord>
+ ]
+
+ function resolve(
+ ...args: MatcherResolveArgs
+ ): NEW_LocationResolved<NEW_MatcherRecord> {
const [location, currentLocation] = args
// string location, e.g. '/foo', '../bar', 'baz', '?page=1'
// parseURL handles relative paths
const url = parseURL(parseQuery, location, currentLocation?.path)
- let matcher: MatcherPattern | undefined
- let matched: NEW_LocationResolved['matched'] | undefined
+ let matcher: NEW_MatcherRecord | undefined
+ let matched:
+ | NEW_LocationResolved<NEW_MatcherRecord>['matched']
+ | undefined
let parsedParams: MatcherParamsFormatted | null | undefined
for (matcher of matchers.values()) {
`Cannot resolve an unnamed relative location without a current location. This will throw in production.`,
location
)
+ const query = normalizeQuery(location.query)
+ const hash = location.hash ?? ''
+ const path = location.path ?? '/'
return {
...NO_MATCH_LOCATION,
- fullPath: '/',
- path: '/',
- query: {},
- hash: '',
+ fullPath: stringifyURL(stringifyQuery, { path, query, hash }),
+ 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)
+ // FIXME: remove once name cannot be null
+ const matcher = name != null && matchers.get(name)
if (!matcher) {
throw new Error(`Matcher "${String(location.name)}" not found`)
}
}
}
- function addRoute(record: MatcherRecordRaw, parent?: MatcherPattern) {
+ function addRoute(record: NEW_MatcherRecordRaw, parent?: NEW_MatcherRecord) {
const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol())
// FIXME: proper normalization of the record
- const normalizedRecord: MatcherPattern = {
+ const normalizedRecord: NEW_MatcherRecord = {
...record,
name,
parent,
addRoute(record)
}
- function removeRoute(matcher: MatcherPattern) {
+ function removeRoute(matcher: NEW_MatcherRecord) {
matchers.delete(matcher.name)
// TODO: delete children and aliases
}
MatcherPatternPath,
MatcherPatternQuery,
MatcherPatternParams_Base,
- MatcherPattern,
} from '../matcher-pattern'
+import { NEW_MatcherRecord } from '../matcher'
import { miss } from './errors'
export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{
export const EMPTY_PATH_ROUTE = {
name: 'no params',
path: EMPTY_PATH_PATTERN_MATCHER,
-} satisfies MatcherPattern
+} satisfies NEW_MatcherRecord
export const USER_ID_ROUTE = {
name: 'user-id',
path: USER_ID_PATH_PATTERN_MATCHER,
-} satisfies MatcherPattern
+} satisfies NEW_MatcherRecord
return typeof route === 'string' || (route && typeof route === 'object')
}
-export function isRouteName(name: any): name is RouteRecordNameGeneric {
+export function isRouteName(
+ name: unknown
+): name is NonNullable<RouteRecordNameGeneric> {
return typeof name === 'string' || typeof name === 'symbol'
}