From be657dba3a7c81f8b30efbb4b4b23503e89cf805 Mon Sep 17 00:00:00 2001 From: skirtle <65301168+skirtles-code@users.noreply.github.com> Date: Thu, 7 Mar 2024 00:07:57 +0000 Subject: [PATCH] feat: pluggable matcher --- packages/router/src/devtools.ts | 10 ++- packages/router/src/index.ts | 16 ++++- packages/router/src/matcher/index.ts | 53 ++++++++++++---- packages/router/src/router.ts | 92 ++++++++++++++++++++-------- 4 files changed, 130 insertions(+), 41 deletions(-) diff --git a/packages/router/src/devtools.ts b/packages/router/src/devtools.ts index b543fa80b..f37082273 100644 --- a/packages/router/src/devtools.ts +++ b/packages/router/src/devtools.ts @@ -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' @@ -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( + app: App, + router: GenericRouter, + matcher: GenericRouterMatcher +) { // Take over router.beforeEach and afterEach // make sure we are not registering the devtool twice diff --git a/packages/router/src/index.ts b/packages/router/src/index.ts index 816dd2c51..4e792d005 100644 --- a/packages/router/src/index.ts +++ b/packages/router/src/index.ts @@ -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 { @@ -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 { diff --git a/packages/router/src/matcher/index.ts b/packages/router/src/matcher/index.ts index bdc292c86..f14c42139 100644 --- a/packages/router/src/matcher/index.ts +++ b/packages/router/src/matcher/index.ts @@ -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 { + addRoute: (record: RC, parent?: RouteRecordMatcher) => () => void removeRoute: { (matcher: RouteRecordMatcher): void (name: RouteRecordName): void @@ -47,6 +47,18 @@ export interface RouterMatcher { ) => MatcherLocation } +/** + * Internal RouterMatcher + * + * @internal + */ +export interface RouterMatcher extends GenericRouterMatcher {} + +// TODO: Should this be considered internal? +export interface CloneableRouterMatcher extends RouterMatcher { + clone: () => CloneableRouterMatcher +} + /** * Creates a Router Matcher. * @@ -57,15 +69,37 @@ export interface RouterMatcher { export function createRouterMatcher( routes: Readonly, globalOptions: PathParserOptions -): RouterMatcher { - // normalized ordered array of matchers - const matchers: RouteRecordMatcher[] = [] - const matcherMap = new Map() +): CloneableRouterMatcher { globalOptions = mergeOptions( { strict: false, end: true, sensitive: false } as PathParserOptions, globalOptions ) + const matcher = createRouterMatcherInternal( + globalOptions, + [], + new Map() + ) + + // add initial routes + routes.forEach(route => matcher.addRoute(route)) + + return matcher +} + +function createRouterMatcherInternal( + globalOptions: PathParserOptions, + matchers: RouteRecordMatcher[], + matcherMap: Map +): CloneableRouterMatcher { + function clone() { + return createRouterMatcherInternal( + globalOptions, + [...matchers], + new Map(matcherMap) + ) + } + function getRecordMatcher(name: RouteRecordName) { return matcherMap.get(name) } @@ -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( diff --git a/packages/router/src/router.ts b/packages/router/src/router.ts index 20ab4d1e2..542f0ae36 100644 --- a/packages/router/src/router.ts +++ b/packages/router/src/router.ts @@ -25,7 +25,12 @@ import { scrollToPosition, _ScrollPositionNormalized, } from './scrollBehavior' -import { createRouterMatcher, PathParserOptions } from './matcher' +import { + CloneableRouterMatcher, + createRouterMatcher, + GenericRouterMatcher, + PathParserOptions, +} from './matcher' import { createRouterError, ErrorTypes, @@ -97,10 +102,11 @@ export interface RouterScrollBehavior { ): Awaitable } -/** - * Options to initialize a {@link Router} instance. - */ -export interface RouterOptions extends PathParserOptions { +export interface DefaultMatcherOptions extends PathParserOptions { + routes: Readonly +} + +export interface SharedOptions { /** * History implementation used by the router. Most web applications should use * `createWebHistory` but it requires the server to be properly configured. @@ -117,10 +123,6 @@ export interface RouterOptions extends PathParserOptions { * ``` */ history: RouterHistory - /** - * Initial list of routes that should be added to the router. - */ - routes: Readonly /** * Function to control scrolling when navigating between pages. Can return a * Promise to delay scrolling. Check {@link ScrollBehavior}. @@ -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 +} + +export interface RouterWithMatcherOptions extends SharedOptions { + matcher: GenericRouterMatcher +} + /** * Router instance. */ -export interface Router { +export interface GenericRouter { /** * @internal */ @@ -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. @@ -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. * @@ -260,12 +276,12 @@ export interface Router { * Go back in history if possible by calling `history.back()`. Equivalent to * `router.go(-1)`. */ - back(): ReturnType + back(): ReturnType['go']> /** * Go forward in history if possible by calling `history.forward()`. * Equivalent to `router.go(1)`. */ - forward(): ReturnType + forward(): ReturnType['go']> /** * Allows you to move forward or backward through the history. Calls * `history.go()`. @@ -352,13 +368,44 @@ export interface Router { install(app: App): void } +export interface Router extends GenericRouter { + readonly options: RouterOptions +} + +export interface RouterWithMatcher extends GenericRouter { + readonly options: RouterWithMatcherOptions +} + +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 as Router +} + +export function createRouterWithMatcher( + options: RouterWithMatcherOptions +): RouterWithMatcher { + const matcher = options.matcher const parseQuery = options.parseQuery || originalParseQuery const stringifyQuery = options.stringifyQuery || originalStringifyQuery const routerHistory = options.history @@ -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) { @@ -1201,7 +1245,7 @@ export function createRouter(options: RouterOptions): Router { let started: boolean | undefined const installedApps = new Set() - const router: Router = { + const router: RouterWithMatcher = { currentRoute, listening: true, @@ -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), @@ -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)