const component = null
+/** @typedef {import('../src/types').RouteRecord} RouteRecord */
+/** @typedef {import('../src/types').MatcherLocation} MatcherLocation */
+/** @typedef {import('../src/types').MatcherLocationRedirect} MatcherLocationRedirect */
+/** @typedef {import('../src/types').MatcherLocationNormalized} MatcherLocationNormalized */
+
describe('Router Matcher', () => {
describe('resolve', () => {
/**
*
- * @param {import('../src/types').RouteRecord} record
- * @param {import('../src/types').MatcherLocation} location
- * @param {Partial<import('../src/types').MatcherLocationNormalized>} resolved
- * @param {import('../src/types').MatcherLocationNormalized} start
+ * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against
+ * @param {MatcherLocation} location location we want to reolve against
+ * @param {Partial<MatcherLocationNormalized>} resolved Expected resolved location given by the matcher
+ * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving
*/
function assertRecordMatch(
record,
resolved,
start = START_LOCATION_NORMALIZED
) {
- const matcher = new RouterMatcher([record])
+ record = Array.isArray(record) ? record : [record]
+ const matcher = new RouterMatcher(record)
const targetLocation = {}
// add location if provided as it should be the same value
resolved.path = location.path
}
- // use one single record
- if (!('matched' in resolved)) resolved.matched = [record]
+ if ('redirect' in record) {
+ } else {
+ // use one single record
+ // @ts-ignore
+ if (!('matched' in resolved)) resolved.matched = record
+ }
// allows not passing params
if ('params' in location) {
/**
*
- * @param {import('../src/types').RouteRecord} record
- * @param {import('../src/types').MatcherLocation} location
- * @param {import('../src/types').MatcherLocationNormalized} start
+ * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against
+ * @param {MatcherLocation} location location we want to reolve against
+ * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving
* @returns {any} error
*/
function assertErrorMatch(
)
})
+ describe('redirects', () => {
+ /**
+ *
+ * @param {RouteRecord[]} records Record or records we are testing the matcher against
+ * @param {MatcherLocation} location location we want to reolve against
+ * @param {MatcherLocationNormalized | MatcherLocationRedirect} expected Expected resolved location given by the matcher
+ * @param {MatcherLocationNormalized} [currentLocation] Optional currentLocation used when resolving
+ */
+ function assertRedirect(
+ records,
+ location,
+ expected,
+ currentLocation = START_LOCATION_NORMALIZED
+ ) {
+ const matcher = new RouterMatcher(records)
+ const resolved = matcher.resolve(location, currentLocation)
+ expect(resolved).toEqual(expected)
+ return resolved
+ }
+
+ // FIXME: refactor the tests into the function, probably use a common set of routes
+ // tests named routes and relatives routes
+ // move to different folder
+
+ it('resolves a redirect string', () => {
+ const records = [
+ { path: '/home', component },
+ { path: '/redirect', redirect: '/home' },
+ ]
+ assertRedirect(
+ records,
+ {
+ name: undefined,
+ path: '/redirect',
+ },
+ {
+ redirect: '/home',
+ normalizedLocation: {
+ path: '/redirect',
+ params: {},
+ name: undefined,
+ matched: [],
+ },
+ }
+ )
+ })
+
+ it('resolves a redirect function that returns a string', () => {
+ const redirect = () => '/home'
+ const records = [
+ { path: '/home', component },
+ { path: '/redirect', redirect },
+ ]
+ assertRedirect(
+ records,
+ {
+ name: undefined,
+ path: '/redirect',
+ },
+ {
+ redirect,
+ normalizedLocation: {
+ path: '/redirect',
+ params: {},
+ name: undefined,
+ matched: [],
+ },
+ }
+ )
+ })
+
+ it('resolves a redirect function that returns an object route', () => {
+ const redirect = () => {
+ path: '/home'
+ }
+ const records = [
+ { path: '/home', component },
+ { path: '/redirect', redirect },
+ ]
+ assertRedirect(
+ records,
+ {
+ name: undefined,
+ path: '/redirect',
+ },
+ {
+ redirect,
+ normalizedLocation: {
+ path: '/redirect',
+ params: {},
+ name: undefined,
+ matched: [],
+ },
+ }
+ )
+ })
+
+ it('resolves a redirect as an object', () => {
+ const records = [
+ { path: '/home', component },
+ { path: '/redirect', redirect: { path: 'home' } },
+ ]
+ assertRedirect(
+ records,
+ {
+ name: undefined,
+ path: '/redirect',
+ },
+ {
+ redirect: { path: 'home' },
+ normalizedLocation: {
+ path: '/redirect',
+ params: {},
+ name: undefined,
+ matched: [],
+ },
+ }
+ )
+ })
+
+ it('normalize a location when redirecting', () => {
+ const redirect = to => ({ name: 'b', params: to.params })
+ const records = [
+ { path: '/home', component },
+ {
+ path: '/a/:a',
+ name: 'a',
+ redirect,
+ },
+ { path: '/b/:a', name: 'b', component },
+ ]
+ assertRedirect(
+ records,
+ {
+ name: undefined,
+ path: '/a/foo',
+ },
+ {
+ redirect,
+ normalizedLocation: {
+ path: '/a/foo',
+ params: { a: 'foo' },
+ name: 'a',
+ matched: [],
+ },
+ }
+ )
+ })
+ })
+
it('throws if the current named route does not exists', () => {
const record = { path: '/', component }
- const start = { name: 'home', params: {}, path: '/', matched: [record] }
+ const start = {
+ name: 'home',
+ params: {},
+ path: '/',
+ matched: [record],
+ }
// the property should be non enumerable
Object.defineProperty(start, 'matched', { enumerable: false })
expect(assertErrorMatch(record, {}, start)).toMatchInlineSnapshot(
return new HTML5History()
}
+/** @type {import('../src/types').RouteRecord[]} */
const routes = [
{ path: '/', component: components.Home },
{ path: '/foo', component: components.Foo },
+ { path: '/to-foo', redirect: '/foo' },
]
describe('Router', () => {
hash: '',
})
})
+
+ // it('redirects with route record redirect')
})
RouteParams,
MatcherLocation,
MatcherLocationNormalized,
+ MatcherLocationRedirect,
} from './types/index'
import { NoRouteMatchError } from './errors'
-// TODO: rename
interface RouteMatcher {
re: RegExp
resolve: (params?: RouteParams) => string
resolve(
location: Readonly<MatcherLocation>,
currentLocation: Readonly<MatcherLocationNormalized>
- // TODO: return type is wrong, should contain fullPath and record/matched
- ): MatcherLocationNormalized {
+ ): MatcherLocationNormalized | MatcherLocationRedirect {
let matcher: RouteMatcher | void
// TODO: refactor with type guards
matcher = this.matchers.find(m => m.re.test(location.path))
if (!matcher) throw new NoRouteMatchError(currentLocation, location)
- // TODO: build up the array with children based on current location
-
- if ('redirect' in matcher.record) throw new Error('TODO')
-
- const matched = [matcher.record]
const params: RouteParams = {}
const result = matcher.re.exec(location.path)
params[key] = value
}
+ if ('redirect' in matcher.record) {
+ const { redirect } = matcher.record
+ return {
+ redirect,
+ normalizedLocation: {
+ name: matcher.record.name,
+ path: location.path,
+ matched: [],
+ params,
+ },
+ }
+ // if redirect is a function we do not have enough information, so we throw
+ // TODO: not use a throw
+ // throw new RedirectInRecord(typeof redirect === 'function' ? {
+ // redirect,
+ // route: { name: matcher.record.name, path: location.path, params, matched: [] }
+ // } : redirect)
+ }
+
+ // TODO: build up the array with children based on current location
+ const matched = [matcher.record]
+
return {
name: matcher.record.name,
/// no need to resolve the path with the matcher as it was provided
TODO,
PostNavigationGuard,
Lazy,
+ MatcherLocation,
} from './types/index'
import { guardToPromiseFn, extractComponentsGuards } from './utils'
this.history.listen((to, from, info) => {
// TODO: check navigation guards
- const matchedRoute = this.matcher.resolve(to, this.currentRoute)
+ const matchedRoute = this.matchLocation(to, this.currentRoute)
// console.log({ to, matchedRoute })
// TODO: navigate
})
}
+ private matchLocation(
+ location: MatcherLocation,
+ currentLocation: MatcherLocationNormalized
+ ): MatcherLocationNormalized {
+ const matchedRoute = this.matcher.resolve(location, currentLocation)
+ if ('redirect' in matchedRoute) {
+ const { redirect, normalizedLocation } = matchedRoute
+ // TODO: add from to a redirect stack?
+ if (typeof redirect === 'string') {
+ // match the redirect instead
+ return this.matchLocation(
+ this.history.utils.normalizeLocation(redirect),
+ currentLocation
+ )
+ } else if (typeof redirect === 'function') {
+ const url = this.history.utils.normalizeLocation(normalizedLocation)
+ const newLocation = redirect({
+ ...normalizedLocation,
+ ...url,
+ })
+
+ if (typeof newLocation === 'string') {
+ return this.matchLocation(
+ this.history.utils.normalizeLocation(newLocation),
+ currentLocation
+ )
+ }
+
+ return this.matchLocation(newLocation, currentLocation)
+ } else {
+ return this.matchLocation(redirect, currentLocation)
+ }
+ } else {
+ return matchedRoute
+ }
+ }
+
/**
* Trigger a navigation, adding an entry to the history stack. Also apply all navigation
* guards first
if (typeof to === 'string' || 'path' in to) {
url = this.history.utils.normalizeLocation(to)
// TODO: should allow a non matching url to allow dynamic routing to work
- location = this.matcher.resolve(url, this.currentRoute)
+ location = this.matchLocation(url, this.currentRoute)
} else {
// named or relative route
// we need to resolve first
- location = this.matcher.resolve(to, this.currentRoute)
+ location = this.matchLocation(to, this.currentRoute)
// intentionally drop current query and hash
url = this.history.utils.normalizeLocation({
query: to.query ? this.history.utils.normalizeQuery(to.query) : {},
beforeEnter?: NavigationGuard
}
-type DynamicRedirect = (to: RouteLocationNormalized) => RouteLocation
+export type RouteRecordRedirectOption =
+ | RouteLocation
+ | ((to: RouteLocationNormalized) => RouteLocation)
interface RouteRecordRedirect extends RouteRecordCommon {
- redirect: RouteLocation | DynamicRedirect
+ redirect: RouteRecordRedirectOption
}
interface RouteRecordSingleView extends RouteRecordCommon {
matched: MatchedRouteRecord[]
}
+// used when the route records requires a redirection
+// with a function call. The matcher isn't able to do it
+// by itself, so it dispatches the information so the router
+// can pick it up
+export interface MatcherLocationRedirect {
+ redirect: RouteRecordRedirectOption
+ normalizedLocation: MatcherLocationNormalized
+}
+
export interface NavigationGuardCallback {
(): void
(location: RouteLocation): void