type App,
} from 'vue'
import { RouterLink } from '../RouterLink'
-import { RouterView } from '../RouterView'
import {
NavigationType,
type HistoryState,
type RouterHistory,
} from '../history/common'
import type { PathParserOptions } from '../matcher'
-import {
- type NEW_MatcherRecordBase,
- type NEW_LocationResolved,
- type NEW_MatcherRecord,
- type NEW_MatcherRecordRaw,
- type NEW_RouterResolver,
-} from '../new-route-resolver/resolver'
+import { type NEW_LocationResolved } from '../new-route-resolver/resolver'
import {
parseQuery as originalParseQuery,
stringifyQuery as originalStringifyQuery,
type RouterScrollBehavior,
} from '../scrollBehavior'
import type {
+ _RouteRecordProps,
NavigationGuardWithThis,
NavigationHookAfter,
RouteLocation,
} from '../typed-routes'
import {
isRouteLocation,
- isRouteName,
Lazy,
+ RawRouteComponent,
RouteLocationOptions,
RouteMeta,
} from '../types'
routerKey,
routerViewLocationKey,
} from '../injectionSymbols'
+import {
+ EXPERIMENTAL_ResolverStatic,
+ EXPERIMENTAL_ResolverStaticRecord,
+} from '../new-route-resolver/resolver-static'
/**
* resolve, reject arguments of Promise constructor
// linkInactiveClass?: string
}
+// TODO: is it worth to have 2 types for the undefined values?
+export interface EXPERIMENTAL_RouteRecordNormalized
+ extends EXPERIMENTAL_ResolverStaticRecord {
+ /**
+ * Arbitrary data attached to the record.
+ */
+ meta: RouteMeta
+
+ // TODO:
+ redirect?: unknown
+
+ /**
+ * Allow passing down params as props to the component rendered by `router-view`.
+ */
+ props: Record<string, _RouteRecordProps>
+
+ /**
+ * {@inheritDoc RouteRecordMultipleViews.components}
+ */
+ components: Record<string, RawRouteComponent>
+
+ /**
+ * Contains the original modules for lazy loaded components.
+ * @internal
+ */
+ mods: Record<string, unknown>
+}
+
/**
* Options to initialize an experimental {@link EXPERIMENTAL_Router} instance.
* @experimental
*/
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<EXPERIMENTAL_RouteRecordRaw[]>
-
+ // TODO: probably need some generic types
+ // TResolver extends NEW_RouterResolver_Base,
+>extends EXPERIMENTAL_RouterOptions_Base {
/**
* Matcher to use to resolve routes.
+ *
* @experimental
*/
- resolver: NEW_RouterResolver<NEW_MatcherRecordRaw, TMatcherRecord>
+ resolver: EXPERIMENTAL_ResolverStatic<EXPERIMENTAL_RouteRecordNormalized>
}
/**
* Router base instance.
+ *
* @experimental This version is not stable, it's meant to replace {@link Router} in the future.
*/
-export interface EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
+export interface EXPERIMENTAL_Router_Base<TRecord> {
+ // NOTE: for dynamic routing we need this
+ // <TRouteRecordRaw, TRouteRecord>
/**
* Current {@link RouteLocationNormalized}
*/
*/
listening: boolean
- /**
- * 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: TRouteRecordRaw
- ): () => void
- /**
- * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router.
- *
- * @param route - Route Record to add
- */
- addRoute(route: TRouteRecordRaw): () => void
-
- /**
- * Remove an existing route by its name.
- *
- * @param name - Name of the route to remove
- */
- removeRoute(name: NonNullable<RouteRecordNameGeneric>): void
-
/**
* Checks if a route with a given name exists
*
/**
* Get a full list of all the {@link RouteRecord | route records}.
*/
- getRoutes(): TRouteRecord[]
-
- /**
- * Delete all routes from the router matcher.
- */
- clearRoutes(): void
+ getRoutes(): TRecord[]
/**
* Returns the {@link RouteLocation | normalized version} of a
install(app: App): void
}
-export interface EXPERIMENTAL_Router<
- TRouteRecordRaw, // extends NEW_MatcherRecordRaw,
- TRouteRecord extends NEW_MatcherRecord
-> extends EXPERIMENTAL_Router_Base<TRouteRecordRaw, TRouteRecord> {
+export interface EXPERIMENTAL_Router
+ // TODO: dynamic routing
+ // <
+ // TRouteRecordRaw, // extends NEW_MatcherRecordRaw,
+ // TRouteRecord extends NEW_MatcherRecord,
+ // >
+ extends EXPERIMENTAL_Router_Base<EXPERIMENTAL_RouteRecordNormalized> {
/**
* Original options object passed to create the Router
*/
- readonly options: EXPERIMENTAL_RouterOptions<TRouteRecord>
-}
-
-export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw {
- /**
- * Arbitrary data attached to the record.
- */
- meta?: RouteMeta
-
- components?: Record<string, unknown>
- component?: unknown
-
- redirect?: unknown
- score: Array<number[]>
+ readonly options: EXPERIMENTAL_RouterOptions
}
-// TODO: is it worth to have 2 types for the undefined values?
-export interface EXPERIMENTAL_RouteRecordNormalized
- extends NEW_MatcherRecordBase<EXPERIMENTAL_RouteRecordNormalized> {
- /**
- * Arbitrary data attached to the record.
- */
- meta: RouteMeta
- group?: boolean
- score: Array<number[]>
-}
-
-function normalizeRouteRecord(
- record: EXPERIMENTAL_RouteRecordRaw
-): EXPERIMENTAL_RouteRecordNormalized {
- // FIXME: implementation
- return {
- name: __DEV__ ? Symbol('anonymous route record') : Symbol(),
- meta: {},
- ...record,
- children: (record.children || []).map(normalizeRouteRecord),
- }
-}
+// export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw {
+// /**
+// * Arbitrary data attached to the record.
+// */
+// meta?: RouteMeta
+//
+// components?: Record<string, unknown>
+// component?: unknown
+//
+// redirect?: unknown
+// // TODO: Not needed
+// score: Array<number[]>
+// }
+//
+//
+// function normalizeRouteRecord(
+// record: EXPERIMENTAL_RouteRecordRaw
+// ): EXPERIMENTAL_RouteRecordNormalized {
+// // FIXME: implementation
+// return {
+// name: __DEV__ ? Symbol('anonymous route record') : Symbol(),
+// meta: {},
+// ...record,
+// children: (record.children || []).map(normalizeRouteRecord),
+// }
+// }
export function experimental_createRouter(
- options: EXPERIMENTAL_RouterOptions<EXPERIMENTAL_RouteRecordNormalized>
-): EXPERIMENTAL_Router<
- EXPERIMENTAL_RouteRecordRaw,
- EXPERIMENTAL_RouteRecordNormalized
-> {
+ options: EXPERIMENTAL_RouterOptions
+): EXPERIMENTAL_Router {
const {
resolver,
parseQuery = originalParseQuery,
history: routerHistory,
} = options
+ // FIXME: can be removed, it was for migration purposes
if (__DEV__ && !routerHistory)
throw new Error(
'Provide the "history" option when calling "createRouter()":' +
let pendingLocation: RouteLocation = START_LOCATION_NORMALIZED
// leave the scrollRestoration if no scrollBehavior is provided
- if (isBrowser && options.scrollBehavior && 'scrollRestoration' in history) {
+ if (isBrowser && options.scrollBehavior) {
history.scrollRestoration = 'manual'
}
- function addRoute(
- parentOrRoute:
- | NonNullable<RouteRecordNameGeneric>
- | EXPERIMENTAL_RouteRecordRaw,
- route?: EXPERIMENTAL_RouteRecordRaw
- ) {
- let parent: Parameters<(typeof resolver)['addMatcher']>[1] | undefined
- let rawRecord: EXPERIMENTAL_RouteRecordRaw
-
- if (isRouteName(parentOrRoute)) {
- parent = resolver.getMatcher(parentOrRoute)
- if (__DEV__ && !parent) {
- warn(
- `Parent route "${String(
- parentOrRoute
- )}" not found when adding child route`,
- route
- )
- }
- rawRecord = route!
- } else {
- rawRecord = parentOrRoute
- }
-
- const addedRecord = resolver.addMatcher(
- normalizeRouteRecord(rawRecord),
- parent
- )
-
- return () => {
- resolver.removeMatcher(addedRecord)
- }
- }
-
- function removeRoute(name: NonNullable<RouteRecordNameGeneric>) {
- const recordMatcher = resolver.getMatcher(name)
- if (recordMatcher) {
- resolver.removeMatcher(recordMatcher)
- } else if (__DEV__) {
- warn(`Cannot remove non-existent route "${String(name)}"`)
- }
- }
-
function getRoutes() {
- return resolver.getMatchers()
+ return resolver.getRecords()
}
function hasRoute(name: NonNullable<RouteRecordNameGeneric>): boolean {
- return !!resolver.getMatcher(name)
+ return !!resolver.getRecord(name)
}
function locationAsObject(
function runWithContext<T>(fn: () => T): T {
const app: App | undefined = installedApps.values().next().value
+ // FIXME: remove safeguard and ensure
// TODO: remove safeguard and bump required minimum version of Vue
// support Vue < 3.3
- return app && typeof app.runWithContext === 'function'
+ return typeof app?.runWithContext === 'function'
? app.runWithContext(fn)
: fn()
}
let started: boolean | undefined
const installedApps = new Set<App>()
- const router: EXPERIMENTAL_Router<
- EXPERIMENTAL_RouteRecordRaw,
- EXPERIMENTAL_RouteRecordNormalized
- > = {
+ const router: EXPERIMENTAL_Router = {
currentRoute,
listening: true,
- addRoute,
- removeRoute,
- clearRoutes: resolver.clearMatchers,
hasRoute,
getRoutes,
resolve,
isReady,
install(app: App) {
- const router = this
- app.component('RouterLink', RouterLink)
- app.component('RouterView', RouterView)
+ // Must be done by user for vapor variants
+ // 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
app.provide(routeLocationKey, shallowReactive(reactiveRoute))
app.provide(routerViewLocationKey, currentRoute)
- const unmountApp = app.unmount
installedApps.add(app)
- app.unmount = function () {
+ app.onUnmount(() => {
installedApps.delete(app)
// the router is not attached to an app anymore
if (installedApps.size < 1) {
started = false
ready = false
}
- unmountApp()
- }
+ })
// TODO: this probably needs to be updated so it can be used by vue-termui
if ((__DEV__ || __FEATURE_PROD_DEVTOOLS__) && isBrowser) {
): [
leavingRecords: RouteRecordNormalized[],
updatingRecords: RouteRecordNormalized[],
- enteringRecords: RouteRecordNormalized[]
+ enteringRecords: RouteRecordNormalized[],
] {
const leavingRecords: RouteRecordNormalized[] = []
const updatingRecords: RouteRecordNormalized[] = []
import type { LocationQueryRaw } from '../query'
-import type { MatcherName } from './resolver'
+import type { RecordName } from './resolver'
+
+// FIXME: rename to ResolverLocation... instead of MatcherLocation... since they are returned by a resolver
/**
* Generic object of params that can be passed to a matcher.
export type EmptyParams = Record<PropertyKey, never>
export interface MatcherLocationAsNamed {
- name: MatcherName
+ name: RecordName
// FIXME: should this be optional?
params: MatcherParamsFormatted
query?: LocationQueryRaw
import { EmptyParams, MatcherParamsFormatted } from './matcher-location'
import { miss } from './matchers/errors'
-export interface MatcherPatternParams_Base<
+/**
+ * Base interface for matcher patterns that extract params from a URL.
+ *
+ * @template TIn - type of the input value to match against the pattern
+ * @template TOut - type of the output value after matching
+ *
+ * In the case of the `path`, the `TIn` is a `string`, but in the case of the
+ * query, it's the object of query params.
+ *
+ * @internal this is the base interface for all matcher patterns, it shouldn't
+ * be used directly
+ */
+export interface MatcherPattern<
TIn = string,
- TOut extends MatcherParamsFormatted = MatcherParamsFormatted
+ TOut extends MatcherParamsFormatted = MatcherParamsFormatted,
> {
/**
* Matches a serialized params value against the pattern.
*
* @param value - params value to parse
* @throws {MatchMiss} if the value doesn't match
- * @returns parsed params
+ * @returns parsed params object
*/
match(value: TIn): TOut
* shouldn't).
*
* @param value - params value to parse
+ * @returns serialized params value
*/
build(params: TOut): TIn
}
+/**
+ * Handles the `path` part of a URL. It can transform a path string into an
+ * object of params and vice versa.
+ */
export interface MatcherPatternPath<
// TODO: should we allow to not return anything? It's valid to spread null and undefined
- TParams extends MatcherParamsFormatted = MatcherParamsFormatted // | null // | undefined // | void // so it might be a bit more convenient
-> extends MatcherPatternParams_Base<string, TParams> {}
+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted, // | null // | undefined // | void // so it might be a bit more convenient
+> extends MatcherPattern<string, TParams> {}
export class MatcherPatternPathStatic
implements MatcherPatternPath<EmptyParams>
}
// example of a static matcher built at runtime
// new MatcherPatternPathStatic('/')
+// new MatcherPatternPathStatic('/team')
export interface Param_GetSet<
TIn extends string | string[] = string | string[],
- TOut = TIn
+ TOut = TIn,
> {
get?: (value: NoInfer<TIn>) => TOut
set?: (value: NoInfer<TOut>) => TIn
}
export class MatcherPatternPathDynamic<
- TParams extends MatcherParamsFormatted = MatcherParamsFormatted
+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
> implements MatcherPatternPath<TParams>
{
private params: Record<string, Required<ParamParser_Generic>> = {}
+
constructor(
private re: RegExp,
params: Record<keyof TParams, ParamParser_Generic>,
// }
}
+/**
+ * Handles the `query` part of a URL. It can transform a query object into an
+ * object of params and vice versa.
+ */
export interface MatcherPatternQuery<
- TParams extends MatcherParamsFormatted = MatcherParamsFormatted
-> extends MatcherPatternParams_Base<MatcherQueryParams, TParams> {}
+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
+> extends MatcherPattern<MatcherQueryParams, TParams> {}
+/**
+ * Handles the `hash` part of a URL. It can transform a hash string into an
+ * object of params and vice versa.
+ */
export interface MatcherPatternHash<
- TParams extends MatcherParamsFormatted = MatcherParamsFormatted
-> extends MatcherPatternParams_Base<string, TParams> {}
+ TParams extends MatcherParamsFormatted = MatcherParamsFormatted,
+> extends MatcherPattern<string, TParams> {}
import { describe, expect, it } from 'vitest'
import { defineComponent } from 'vue'
-import { RouteComponent, RouteRecordRaw } from '../types'
+import { RouteComponent, RouteMeta, RouteRecordRaw } from '../types'
import { NEW_stringifyURL } from '../location'
import { mockWarn } from '../../__tests__/vitest-mock-warn'
import {
} from './resolver'
import { miss } from './matchers/errors'
import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern'
-import { type EXPERIMENTAL_RouteRecordRaw } from '../experimental/router'
+import { EXPERIMENTAL_RouterOptions } from '../experimental/router'
import { stringifyQuery } from '../query'
import type {
MatcherLocationAsNamed,
import { tokenizePath } from '../matcher/pathTokenizer'
import { mergeOptions } from '../utils'
+// FIXME: this type was removed, it will be a new one once a dynamic resolver is implemented
+export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw {
+ /**
+ * Arbitrary data attached to the record.
+ */
+ meta?: RouteMeta
+
+ components?: Record<string, unknown>
+ component?: unknown
+
+ redirect?: unknown
+ score: Array<number[]>
+ readonly options: EXPERIMENTAL_RouterOptions
+}
+
// for raw route record
const component: RouteComponent = defineComponent({})
// for normalized route records
| MatcherLocationAsPathAbsolute = START_LOCATION
) {
const records = (Array.isArray(record) ? record : [record]).map(
- (record): EXPERIMENTAL_RouteRecordRaw =>
+ (record): NEW_MatcherRecordRaw =>
isExperimentalRouteRecordRaw(record)
? { components, ...record }
: compileRouteRecord(record)
* NOTE: for these classes to keep the same code we need to tell TS with `"useDefineForClassFields": true` in the `tsconfig.json`
*/
+// TODO: document helpers if kept. The helpers could also be moved to the generated code to reduce bundle size. After all, user is unlikely to write these manually
+
/**
* Error throw when a matcher miss
*/
// NOTE: not sure about having a helper. Using `new MatchMiss(description?)` is good enough
export const miss = () => new MatchMiss()
+// TODO: which one?, the return type of never makes types work anyway
+// export const throwMiss = () => { throw new MatchMiss() }
+// export const throwMiss = (...args: ConstructorParameters<typeof MatchMiss>) => { throw new MatchMiss(...args) }
/**
* Error throw when a param is invalid when parsing params from path, query, or hash.
import {
MatcherPatternPath,
MatcherPatternQuery,
- MatcherPatternParams_Base,
+ MatcherPatternHash,
} from '../matcher-pattern'
import { NEW_MatcherRecord } from '../resolver'
-import { miss } from './errors'
+import { invalid, miss } from './errors'
export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{
pathMatch: string
}
const id = Number(match[1])
if (Number.isNaN(id)) {
- throw miss()
+ throw invalid('id')
+ // throw miss()
}
return { id }
},
}
},
build: params => ({ page: String(params.page) }),
- } satisfies MatcherPatternQuery<{ page: number }>
+ }
-export const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base<
- string,
- { hash: string | null }
-> = {
+export const ANY_HASH_PATTERN_MATCHER: MatcherPatternHash<// hash could be named anything, in this case it creates a param named hash
+{ hash: string | null }> = {
match: hash => ({ hash: hash ? hash.slice(1) : null }),
build: ({ hash }) => (hash ? `#${hash}` : ''),
}
--- /dev/null
+import { normalizeQuery, parseQuery, stringifyQuery } from '../query'
+import {
+ LocationNormalized,
+ NEW_stringifyURL,
+ parseURL,
+ resolveRelativePath,
+} from '../location'
+import {
+ MatcherLocationAsNamed,
+ MatcherLocationAsPathAbsolute,
+ MatcherLocationAsPathRelative,
+ MatcherLocationAsRelative,
+ MatcherParamsFormatted,
+} from './matcher-location'
+import {
+ buildMatched,
+ EXPERIMENTAL_ResolverRecord_Base,
+ RecordName,
+ MatcherQueryParams,
+ NEW_LocationResolved,
+ NEW_RouterResolver_Base,
+ NO_MATCH_LOCATION,
+} from './resolver'
+
+export interface EXPERIMENTAL_ResolverStaticRecord
+ extends EXPERIMENTAL_ResolverRecord_Base {}
+
+export interface EXPERIMENTAL_ResolverStatic<TRecord>
+ extends NEW_RouterResolver_Base<TRecord> {}
+
+export function createStaticResolver<
+ TRecord extends EXPERIMENTAL_ResolverStaticRecord,
+>(records: TRecord[]): EXPERIMENTAL_ResolverStatic<TRecord> {
+ // allows fast access to a matcher by name
+ const recordMap = new Map<RecordName, TRecord>()
+ for (const record of records) {
+ recordMap.set(record.name, record)
+ }
+
+ // NOTE: because of the overloads, we need to manually type the arguments
+ type _resolveArgs =
+ | [absoluteLocation: `/${string}`, currentLocation?: undefined]
+ | [relativeLocation: string, currentLocation: NEW_LocationResolved<TRecord>]
+ | [
+ absoluteLocation: MatcherLocationAsPathAbsolute,
+ // Same as above
+ // currentLocation?: NEW_LocationResolved<TRecord> | undefined
+ currentLocation?: undefined,
+ ]
+ | [
+ relativeLocation: MatcherLocationAsPathRelative,
+ currentLocation: NEW_LocationResolved<TRecord>,
+ ]
+ | [
+ location: MatcherLocationAsNamed,
+ // Same as above
+ // currentLocation?: NEW_LocationResolved<TRecord> | undefined
+ currentLocation?: undefined,
+ ]
+ | [
+ relativeLocation: MatcherLocationAsRelative,
+ currentLocation: NEW_LocationResolved<TRecord>,
+ ]
+
+ function resolve(
+ ...[to, currentLocation]: _resolveArgs
+ ): NEW_LocationResolved<TRecord> {
+ if (typeof to === 'object' && (to.name || to.path == null)) {
+ // relative location by path or by name
+ if (__DEV__ && to.name == null && currentLocation == null) {
+ console.warn(
+ `Cannot resolve relative location "${JSON.stringify(to)}"without a current location. This will throw in production.`,
+ to
+ )
+ // NOTE: normally there is no query, hash or path but this helps debug
+ // what kind of object location was passed
+ // @ts-expect-error: to is never
+ const query = normalizeQuery(to.query)
+ // @ts-expect-error: to is never
+ const hash = to.hash ?? ''
+ // @ts-expect-error: to is never
+ const path = to.path ?? '/'
+ return {
+ ...NO_MATCH_LOCATION,
+ fullPath: NEW_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 = to.name ?? currentLocation!.name
+ const record = recordMap.get(name)!
+ if (__DEV__ && (!record || !name)) {
+ throw new Error(`Record "${String(name)}" not found`)
+ }
+
+ // unencoded params in a formatted form that the user came up with
+ const params: MatcherParamsFormatted = {
+ ...currentLocation?.params,
+ ...to.params,
+ }
+ const path = record.path.build(params)
+ const hash = record.hash?.build(params) ?? ''
+ const matched = buildMatched(record)
+ const query = Object.assign(
+ {
+ ...currentLocation?.query,
+ ...normalizeQuery(to.query),
+ },
+ ...matched.map(record => record.query?.build(params))
+ )
+
+ return {
+ name,
+ fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash),
+ path,
+ query,
+ hash,
+ params,
+ matched,
+ }
+ // string location, e.g. '/foo', '../bar', 'baz', '?page=1'
+ } else {
+ // parseURL handles relative paths
+ let url: LocationNormalized
+ if (typeof to === 'string') {
+ url = parseURL(parseQuery, to, currentLocation?.path)
+ } else {
+ const query = normalizeQuery(to.query)
+ url = {
+ fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash),
+ path: resolveRelativePath(to.path, currentLocation?.path || '/'),
+ query,
+ hash: to.hash || '',
+ }
+ }
+
+ let record: TRecord | undefined
+ let matched: NEW_LocationResolved<TRecord>['matched'] | undefined
+ let parsedParams: MatcherParamsFormatted | null | undefined
+
+ for (record of records) {
+ // match the path because the path matcher only needs to be matched here
+ // match the hash because only the deepest child matters
+ // End up by building up the matched array, (reversed so it goes from
+ // root to child) and then match and merge all queries
+ try {
+ const pathParams = record.path.match(url.path)
+ const hashParams = record.hash?.match(url.hash)
+ matched = buildMatched(record)
+ const queryParams: MatcherQueryParams = Object.assign(
+ {},
+ ...matched.map(record => record.query?.match(url.query))
+ )
+ // TODO: test performance
+ // for (const record of matched) {
+ // Object.assign(queryParams, record.query?.match(url.query))
+ // }
+
+ parsedParams = { ...pathParams, ...queryParams, ...hashParams }
+ // we found our match!
+ break
+ } catch (e) {
+ // for debugging tests
+ // console.log('❌ ERROR matching', e)
+ }
+ }
+
+ // No match location
+ if (!parsedParams || !matched) {
+ return {
+ ...url,
+ ...NO_MATCH_LOCATION,
+ // already decoded
+ // query: url.query,
+ // hash: url.hash,
+ }
+ }
+
+ return {
+ ...url,
+ // record exists if matched exists
+ name: record!.name,
+ params: parsedParams,
+ matched,
+ }
+ // TODO: handle object location { path, query, hash }
+ }
+ }
+
+ return {
+ resolve,
+ getRecords: () => records,
+ getRecord: name => recordMap.get(name),
+ }
+}
pathEncoded,
} from './resolver'
import {
- MatcherPatternParams_Base,
- MatcherPatternPath,
MatcherPatternQuery,
MatcherPatternPathStatic,
MatcherPatternPathDynamic,
} from './matcher-pattern'
-import { miss } from './matchers/errors'
-import { EmptyParams } from './matcher-location'
import {
EMPTY_PATH_ROUTE,
USER_ID_ROUTE,
ANY_PATH_ROUTE,
+ ANY_PATH_PATTERN_MATCHER,
+ EMPTY_PATH_PATTERN_MATCHER,
+ USER_ID_PATH_PATTERN_MATCHER,
+ ANY_HASH_PATTERN_MATCHER,
} from './matchers/test-utils'
-const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ pathMatch: string }> = {
- match(path) {
- return { pathMatch: path }
- },
- build({ pathMatch }) {
- return pathMatch
- },
-}
-
-const EMPTY_PATH_PATTERN_MATCHER: MatcherPatternPath<EmptyParams> = {
- match: path => {
- if (path !== '/') {
- throw miss()
- }
- return {}
- },
- build: () => '/',
-}
-
-const USER_ID_PATH_PATTERN_MATCHER: MatcherPatternPath<{ id: number }> = {
- match(value) {
- const match = value.match(/^\/users\/(\d+)$/)
- if (!match?.[1]) {
- throw miss()
- }
- const id = Number(match[1])
- if (Number.isNaN(id)) {
- throw miss()
- }
- return { id }
- },
- build({ id }) {
- return `/users/${id}`
- },
-}
-
const PAGE_QUERY_PATTERN_MATCHER: MatcherPatternQuery<{ page: number }> = {
match: query => {
const page = Number(query.page)
build: params => ({ page: String(params.page) }),
} satisfies MatcherPatternQuery<{ page: number }>
-const ANY_HASH_PATTERN_MATCHER: MatcherPatternParams_Base<
- string,
- { hash: string | null }
-> = {
- match: hash => ({ hash: hash ? hash.slice(1) : null }),
- build: ({ hash }) => (hash ? `#${hash}` : ''),
-}
-
describe('RouterMatcher', () => {
describe('new matchers', () => {
it('static path', () => {
stringifyQuery,
} from '../query'
import type {
+ MatcherPattern,
MatcherPatternHash,
MatcherPatternPath,
MatcherPatternQuery,
/**
* Allowed types for a matcher name.
*/
-export type MatcherName = string | symbol
+export type RecordName = string | symbol
/**
* 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 addMatcher}.
- * - `TMatcherRecord` represents the normalized record type returned by {@link getMatchers}.
+ * - `TMatcherRecord` represents the normalized record type returned by {@link getRecords}.
*/
-export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord> {
+export interface NEW_RouterResolver_Base<TRecord> {
/**
* Resolves an absolute location (like `/path/to/somewhere`).
+ *
+ * @param absoluteLocation - The absolute location to resolve.
+ * @param currentLocation - This value is ignored and should not be passed if the location is absolute.
*/
resolve(
absoluteLocation: `/${string}`,
currentLocation?: undefined
- ): NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TRecord>
/**
* Resolves a string location relative to another location. A relative location can be `./same-folder`,
*/
resolve(
relativeLocation: string,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
- ): NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TRecord>
+ ): NEW_LocationResolved<TRecord>
/**
* Resolves a location by its name. Any required params or query must be passed in the `options` argument.
// TODO: is this useful?
currentLocation?: undefined
// currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
- ): NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TRecord>
/**
* Resolves a location by its absolute path (starts with `/`). Any required query must be passed.
// TODO: is this useful?
currentLocation?: undefined
// currentLocation?: NEW_LocationResolved<TMatcherRecord> | undefined
- ): NEW_LocationResolved<TMatcherRecord>
+ ): NEW_LocationResolved<TRecord>
resolve(
location: MatcherLocationAsPathRelative,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
- ): NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TRecord>
+ ): NEW_LocationResolved<TRecord>
// NOTE: in practice, this overload can cause bugs. It's better to use named locations
*/
resolve(
relativeLocation: MatcherLocationAsRelative,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
- ): NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TRecord>
+ ): NEW_LocationResolved<TRecord>
+ /**
+ * Get a list of all resolver records.
+ * Previously named `getRoutes()`
+ */
+ getRecords(): TRecord[]
+
+ /**
+ * Get a resolver record by its name.
+ * Previously named `getRecordMatcher()`
+ */
+ getRecord(name: RecordName): TRecord | undefined
+}
+
+/**
+ * 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 addMatcher}.
+ * - `TMatcherRecord` represents the normalized record type returned by {@link getRecords}.
+ */
+export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord>
+ extends NEW_RouterResolver_Base<TMatcherRecord> {
/**
* Add a matcher record. Previously named `addRoute()`.
* @param matcher - The matcher record to add.
* Remove all matcher records. Prevoisly named `clearRoutes()`.
*/
clearMatchers(): void
-
- /**
- * Get a list of all matchers.
- * Previously named `getRoutes()`
- */
- getMatchers(): TMatcherRecord[]
-
- /**
- * Get a matcher by its name.
- * Previously named `getRecordMatcher()`
- */
- getMatcher(name: MatcherName): TMatcherRecord | undefined
}
/**
| MatcherLocationAsPathRelative
| MatcherLocationAsRelative
+// TODO: ResolverLocationResolved
export interface NEW_LocationResolved<TMatched> {
- // FIXME: remove `undefined`
- name: MatcherName | undefined
- // TODO: generics?
+ name: RecordName
params: MatcherParamsFormatted
fullPath: string
*/
export type MatcherPathParams = Record<string, MatcherPathParamsValue>
+// TODO: move to matcher-pattern
export type MatcherQueryParamsValue = string | null | Array<string | null>
export type MatcherQueryParams = Record<string, MatcherQueryParamsValue>
* Name for the route record. Must be unique. Will be set to `Symbol()` if
* not set.
*/
- name?: MatcherName
+ name?: RecordName
/**
* Array of nested routes.
score: Array<number[]>
}
-export interface NEW_MatcherRecordBase<T> {
+export interface EXPERIMENTAL_ResolverRecord_Base {
/**
* Name of the matcher. Unique across all matchers.
*/
- name: MatcherName
+ name: RecordName
+ /**
+ * {@link MatcherPattern} for the path section of the URI.
+ */
path: MatcherPatternPath
+
+ /**
+ * {@link MatcherPattern} for the query section of the URI.
+ */
query?: MatcherPatternQuery
+
+ /**
+ * {@link MatcherPattern} for the hash section of the URI.
+ */
hash?: MatcherPatternHash
- parent?: T
- children: T[]
+ // TODO: here or in router
+ // redirect?: RouteRecordRedirectOption
+ parent?: this
+ children: this[]
+ aliasOf?: this
+
+ /**
+ * Is this a record that groups children. Cannot be matched
+ */
group?: boolean
- aliasOf?: NEW_MatcherRecord
+}
+
+export interface NEW_MatcherDynamicRecord
+ extends EXPERIMENTAL_ResolverRecord_Base {
+ // TODO: the score shouldn't be always needed, it's only needed with dynamic routing
score: Array<number[]>
}
/**
* Normalized version of a {@link NEW_MatcherRecordRaw} record.
*/
-export interface NEW_MatcherRecord
- extends NEW_MatcherRecordBase<NEW_MatcherRecord> {}
+export interface NEW_MatcherRecord extends NEW_MatcherDynamicRecord {}
/**
* Tagged template helper to encode params into a path. Doesn't work with null
/**
* Build the `matched` array of a record that includes all parent records from the root to the current one.
*/
-function buildMatched<T extends NEW_MatcherRecordBase<T>>(record: T): T[] {
+export function buildMatched<T extends EXPERIMENTAL_ResolverRecord_Base>(
+ record: T
+): T[] {
const matched: T[] = []
let node: T | undefined = record
while (node) {
}
export function createCompiledMatcher<
- TMatcherRecord extends NEW_MatcherRecordBase<TMatcherRecord>
+ TMatcherRecord extends NEW_MatcherDynamicRecord,
>(
records: NEW_MatcherRecordRaw[] = []
): NEW_RouterResolver<NEW_MatcherRecordRaw, TMatcherRecord> {
// TODO: we also need an array that has the correct order
- const matcherMap = new Map<MatcherName, TMatcherRecord>()
+ const matcherMap = new Map<RecordName, TMatcherRecord>()
const matchers: TMatcherRecord[] = []
// TODO: allow custom encode/decode functions
| [absoluteLocation: `/${string}`, currentLocation?: undefined]
| [
relativeLocation: string,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TMatcherRecord>,
]
| [
absoluteLocation: MatcherLocationAsPathAbsolute,
// Same as above
// currentLocation?: NEW_LocationResolved<TMatcherRecord> | undefined
- currentLocation?: undefined
+ currentLocation?: undefined,
]
| [
relativeLocation: MatcherLocationAsPathRelative,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TMatcherRecord>,
]
| [
location: MatcherLocationAsNamed,
// Same as above
// currentLocation?: NEW_LocationResolved<TMatcherRecord> | undefined
- currentLocation?: undefined
+ currentLocation?: undefined,
]
| [
relativeLocation: MatcherLocationAsRelative,
- currentLocation: NEW_LocationResolved<TMatcherRecord>
+ currentLocation: NEW_LocationResolved<TMatcherRecord>,
]
function resolve(
matcherMap.clear()
}
- function getMatchers() {
+ function getRecords() {
return matchers
}
- function getMatcher(name: MatcherName) {
+ function getRecord(name: RecordName) {
return matcherMap.get(name)
}
addMatcher,
removeMatcher,
clearMatchers,
- getMatcher,
- getMatchers,
+ getRecord,
+ getRecords,
}
}
* @param matcher - new matcher to be inserted
* @param matchers - existing matchers
*/
-function findInsertionIndex<T extends NEW_MatcherRecordBase<T>>(
+function findInsertionIndex<T extends NEW_MatcherDynamicRecord>(
matcher: T,
matchers: T[]
) {
return upper
}
-function getInsertionAncestor<T extends NEW_MatcherRecordBase<T>>(matcher: T) {
+function getInsertionAncestor<T extends NEW_MatcherDynamicRecord>(matcher: T) {
let ancestor: T | undefined = matcher
while ((ancestor = ancestor.parent)) {
* Checks if a record or any of its parent is an alias
* @param record
*/
-function isAliasRecord<T extends NEW_MatcherRecordBase<T>>(
+function isAliasRecord<T extends NEW_MatcherDynamicRecord>(
record: T | undefined
): boolean {
while (record) {
* Router instance.
*/
export interface Router
- extends EXPERIMENTAL_Router_Base<RouteRecordRaw, RouteRecordNormalized> {
+ extends EXPERIMENTAL_Router_Base<RouteRecordNormalized> {
/**
* Original options object passed to create the Router
*/
readonly options: RouterOptions
+
+ /**
+ * 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
+ ): () => void
+ /**
+ * Add a new {@link EXPERIMENTAL_RouteRecordRaw | route record} to the router.
+ *
+ * @param route - Route Record to add
+ */
+ addRoute(route: RouteRecordRaw): () => void
+
+ /**
+ * Remove an existing route by its name.
+ *
+ * @param name - Name of the route to remove
+ */
+ removeRoute(name: NonNullable<RouteRecordNameGeneric>): void
+
+ /**
+ * Delete all routes from the router.
+ */
+ clearRoutes(): void
}
/**
-export * from '../dist/vue-router'
-// export * from '../src'
+// export * from '../dist/vue-router'
+export * from '../src'