Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: pluggable matcher #2166

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions packages/router/src/devtools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
import { watch } from 'vue'
import { decode } from './encoding'
import { isSameRouteRecord } from './location'
import { RouterMatcher } from './matcher'
import { GenericRouterMatcher } from './matcher'
import { RouteRecordMatcher } from './matcher/pathMatcher'
import { PathParser } from './matcher/pathParserRanker'
import { Router } from './router'
import { GenericRouter } from './router'
import { UseLinkDevtoolsContext } from './RouterLink'
import { RouterViewDevtoolsContext } from './RouterView'
import { RouteLocationNormalized } from './types'
Expand Down Expand Up @@ -59,7 +59,11 @@ function formatDisplay(display: string) {
// to support multiple router instances
let routerId = 0

export function addDevtools(app: App, router: Router, matcher: RouterMatcher) {
export function addDevtools<RC>(
app: App,
router: GenericRouter<RC>,
matcher: GenericRouterMatcher<RC>
) {
// Take over router.beforeEach and afterEach

// make sure we are not registering the devtool twice
Expand Down
16 changes: 13 additions & 3 deletions packages/router/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export { createWebHistory } from './history/html5'
export { createMemoryHistory } from './history/memory'
export { createWebHashHistory } from './history/hash'
export { createRouterMatcher } from './matcher'
export type { RouterMatcher } from './matcher'
export type { GenericRouterMatcher, RouterMatcher } from './matcher'

export { parseQuery, stringifyQuery } from './query'
export type {
Expand Down Expand Up @@ -67,8 +67,18 @@ export type {
NavigationHookAfter,
} from './types'

export { createRouter } from './router'
export type { Router, RouterOptions, RouterScrollBehavior } from './router'
export {
createRouter,
createRouterWithMatcher,
createDefaultMatcher,
} from './router'
export type {
Router,
RouterWithMatcher,
RouterOptions,
RouterWithMatcherOptions,
RouterScrollBehavior,
} from './router'

export { NavigationFailureType, isNavigationFailure } from './errors'
export type {
Expand Down
53 changes: 42 additions & 11 deletions packages/router/src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ import { warn } from '../warning'
import { assign, noop } from '../utils'

/**
* Internal RouterMatcher
* Internal GenericRouterMatcher
*
* @internal
*/
export interface RouterMatcher {
addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
export interface GenericRouterMatcher<RC> {
addRoute: (record: RC, parent?: RouteRecordMatcher) => () => void
removeRoute: {
(matcher: RouteRecordMatcher): void
(name: RouteRecordName): void
Expand All @@ -47,6 +47,18 @@ export interface RouterMatcher {
) => MatcherLocation
}

/**
* Internal RouterMatcher
*
* @internal
*/
export interface RouterMatcher extends GenericRouterMatcher<RouteRecordRaw> {}

// TODO: Should this be considered internal?
export interface CloneableRouterMatcher extends RouterMatcher {
clone: () => CloneableRouterMatcher
}

/**
* Creates a Router Matcher.
*
Expand All @@ -57,15 +69,37 @@ export interface RouterMatcher {
export function createRouterMatcher(
routes: Readonly<RouteRecordRaw[]>,
globalOptions: PathParserOptions
): RouterMatcher {
// normalized ordered array of matchers
const matchers: RouteRecordMatcher[] = []
const matcherMap = new Map<RouteRecordName, RouteRecordMatcher>()
): CloneableRouterMatcher {
globalOptions = mergeOptions(
{ strict: false, end: true, sensitive: false } as PathParserOptions,
globalOptions
)

const matcher = createRouterMatcherInternal(
globalOptions,
[],
new Map<RouteRecordName, RouteRecordMatcher>()
)

// add initial routes
routes.forEach(route => matcher.addRoute(route))

return matcher
}

function createRouterMatcherInternal(
globalOptions: PathParserOptions,
matchers: RouteRecordMatcher[],
matcherMap: Map<RouteRecordName, RouteRecordMatcher>
): CloneableRouterMatcher {
function clone() {
return createRouterMatcherInternal(
globalOptions,
[...matchers],
new Map(matcherMap)
)
}

function getRecordMatcher(name: RouteRecordName) {
return matcherMap.get(name)
}
Expand Down Expand Up @@ -350,10 +384,7 @@ export function createRouterMatcher(
}
}

// add initial routes
routes.forEach(route => addRoute(route))

return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher }
return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher, clone }
}

function paramsFromLocation(
Expand Down
92 changes: 68 additions & 24 deletions packages/router/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ import {
scrollToPosition,
_ScrollPositionNormalized,
} from './scrollBehavior'
import { createRouterMatcher, PathParserOptions } from './matcher'
import {
CloneableRouterMatcher,
createRouterMatcher,
GenericRouterMatcher,
PathParserOptions,
} from './matcher'
import {
createRouterError,
ErrorTypes,
Expand Down Expand Up @@ -97,10 +102,11 @@ export interface RouterScrollBehavior {
): Awaitable<ScrollPosition | false | void>
}

/**
* Options to initialize a {@link Router} instance.
*/
export interface RouterOptions extends PathParserOptions {
export interface DefaultMatcherOptions extends PathParserOptions {
routes: Readonly<RouteRecordRaw[]>
}

export interface SharedOptions {
/**
* History implementation used by the router. Most web applications should use
* `createWebHistory` but it requires the server to be properly configured.
Expand All @@ -117,10 +123,6 @@ export interface RouterOptions extends PathParserOptions {
* ```
*/
history: RouterHistory
/**
* Initial list of routes that should be added to the router.
*/
routes: Readonly<RouteRecordRaw[]>
/**
* Function to control scrolling when navigating between pages. Can return a
* Promise to delay scrolling. Check {@link ScrollBehavior}.
Expand Down Expand Up @@ -174,10 +176,24 @@ export interface RouterOptions extends PathParserOptions {
// linkInactiveClass?: string
}

/**
* Options to initialize a {@link Router} instance.
*/
export interface RouterOptions extends SharedOptions, PathParserOptions {
/**
* Initial list of routes that should be added to the router.
*/
routes: Readonly<RouteRecordRaw[]>
}

export interface RouterWithMatcherOptions<RC> extends SharedOptions {
matcher: GenericRouterMatcher<RC>
}

/**
* Router instance.
*/
export interface Router {
export interface GenericRouter<RC> {
/**
* @internal
*/
Expand All @@ -189,7 +205,7 @@ export interface Router {
/**
* Original options object passed to create the Router
*/
readonly options: RouterOptions
readonly options: SharedOptions

/**
* Allows turning off the listening of history events. This is a low level api for micro-frontends.
Expand All @@ -202,13 +218,13 @@ export interface Router {
* @param parentName - Parent Route Record where `route` should be appended at
* @param route - Route Record to add
*/
addRoute(parentName: RouteRecordName, route: RouteRecordRaw): () => void
addRoute(parentName: RouteRecordName, route: RC): () => void
/**
* Add a new {@link RouteRecordRaw | route record} to the router.
*
* @param route - Route Record to add
*/
addRoute(route: RouteRecordRaw): () => void
addRoute(route: RC): () => void
/**
* Remove an existing route by its name.
*
Expand Down Expand Up @@ -260,12 +276,12 @@ export interface Router {
* Go back in history if possible by calling `history.back()`. Equivalent to
* `router.go(-1)`.
*/
back(): ReturnType<Router['go']>
back(): ReturnType<GenericRouter<RC>['go']>
/**
* Go forward in history if possible by calling `history.forward()`.
* Equivalent to `router.go(1)`.
*/
forward(): ReturnType<Router['go']>
forward(): ReturnType<GenericRouter<RC>['go']>
/**
* Allows you to move forward or backward through the history. Calls
* `history.go()`.
Expand Down Expand Up @@ -352,13 +368,44 @@ export interface Router {
install(app: App): void
}

export interface Router extends GenericRouter<RouteRecordRaw> {
readonly options: RouterOptions
}

export interface RouterWithMatcher<RC> extends GenericRouter<RC> {
readonly options: RouterWithMatcherOptions<RC>
}

export function createDefaultMatcher(
options: DefaultMatcherOptions
): CloneableRouterMatcher {
return createRouterMatcher(options.routes, options)
}

/**
* Creates a Router instance that can be used by a Vue app.
*
* @param options - {@link RouterOptions}
*/
export function createRouter(options: RouterOptions): Router {
const matcher = createRouterMatcher(options.routes, options)
const matcher = createDefaultMatcher(options)

const router = createRouterWithMatcher({
...options,
matcher,
})

// Set the original options
;(router as any).options = options

// Casting needed due to the 'options' property
return router as GenericRouter<RouteRecordRaw> as Router
}

export function createRouterWithMatcher<RC>(
options: RouterWithMatcherOptions<RC>
): RouterWithMatcher<RC> {
const matcher = options.matcher
const parseQuery = options.parseQuery || originalParseQuery
const stringifyQuery = options.stringifyQuery || originalStringifyQuery
const routerHistory = options.history
Expand Down Expand Up @@ -390,12 +437,9 @@ export function createRouter(options: RouterOptions): Router {
// @ts-expect-error: intentionally avoid the type check
applyToParams.bind(null, decode)

function addRoute(
parentOrRoute: RouteRecordName | RouteRecordRaw,
route?: RouteRecordRaw
) {
function addRoute(parentOrRoute: RouteRecordName | RC, route?: RC) {
let parent: Parameters<(typeof matcher)['addRoute']>[1] | undefined
let record: RouteRecordRaw
let record: RC
if (isRouteName(parentOrRoute)) {
parent = matcher.getRecordMatcher(parentOrRoute)
if (__DEV__ && !parent) {
Expand Down Expand Up @@ -1201,7 +1245,7 @@ export function createRouter(options: RouterOptions): Router {
let started: boolean | undefined
const installedApps = new Set<App>()

const router: Router = {
const router: RouterWithMatcher<RC> = {
currentRoute,
listening: true,

Expand Down Expand Up @@ -1230,7 +1274,7 @@ export function createRouter(options: RouterOptions): Router {
app.component('RouterLink', RouterLink)
app.component('RouterView', RouterView)

app.config.globalProperties.$router = router
app.config.globalProperties.$router = router as any
Object.defineProperty(app.config.globalProperties, '$route', {
enumerable: true,
get: () => unref(currentRoute),
Expand Down Expand Up @@ -1261,7 +1305,7 @@ export function createRouter(options: RouterOptions): Router {
})
}

app.provide(routerKey, router)
app.provide(routerKey, router as any)
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)

Expand Down
Loading