From: Eduardo San Martin Morote Date: Fri, 25 Jul 2025 13:33:47 +0000 (+0200) Subject: refactor: reorg resolver X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=4ccd88a787ad1d0b5ddaf19fd33fa16ec3259023;p=thirdparty%2Fvuejs%2Frouter.git refactor: reorg resolver --- diff --git a/packages/router/src/experimental/index.ts b/packages/router/src/experimental/index.ts index 8af1615a..62ae84c6 100644 --- a/packages/router/src/experimental/index.ts +++ b/packages/router/src/experimental/index.ts @@ -17,15 +17,15 @@ export { createStaticResolver } from './route-resolver/resolver-static' export type { MatcherQueryParams, MatcherQueryParamsValue, -} from './route-resolver/resolver' +} from './route-resolver/resolver-abstract' export { MatcherPatternPathDynamic, MatcherPatternPathStatic, MatcherPatternPathStar, -} from './route-resolver/matcher-pattern' +} from './route-resolver/matchers/matcher-pattern' export type { MatcherPattern, MatcherPatternHash, MatcherPatternPath, MatcherPatternQuery, -} from './route-resolver/matcher-pattern' +} from './route-resolver/matchers/matcher-pattern' diff --git a/packages/router/src/experimental/route-resolver/index.ts b/packages/router/src/experimental/route-resolver/index.ts deleted file mode 100644 index 4c07b32c..00000000 --- a/packages/router/src/experimental/route-resolver/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { createCompiledMatcher } from './resolver' diff --git a/packages/router/src/experimental/route-resolver/matcher-location.ts b/packages/router/src/experimental/route-resolver/matcher-location.ts deleted file mode 100644 index 8d955b7b..00000000 --- a/packages/router/src/experimental/route-resolver/matcher-location.ts +++ /dev/null @@ -1,64 +0,0 @@ -import type { LocationQueryRaw } from '../../query' -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 MatcherParamsFormatted = Record - -/** - * Empty object in TS. - */ -export type EmptyParams = Record - -export interface MatcherLocationAsNamed { - name: RecordName - // FIXME: should this be optional? - params: MatcherParamsFormatted - query?: LocationQueryRaw - hash?: string - - /** - * @deprecated This is ignored when `name` is provided - */ - path?: undefined -} - -export interface MatcherLocationAsPathRelative { - path: string - query?: LocationQueryRaw - hash?: string - - /** - * @deprecated This is ignored when `path` is provided - */ - name?: undefined - /** - * @deprecated This is ignored when `path` (instead of `name`) is provided - */ - params?: undefined -} - -// TODO: does it make sense to support absolute paths objects? - -export interface MatcherLocationAsPathAbsolute - extends MatcherLocationAsPathRelative { - path: `/${string}` -} - -export interface MatcherLocationAsRelative { - params?: MatcherParamsFormatted - query?: LocationQueryRaw - hash?: string - - /** - * @deprecated This location is relative to the next parameter. This `name` will be ignored. - */ - name?: undefined - /** - * @deprecated This location is relative to the next parameter. This `path` will be ignored. - */ - path?: undefined -} diff --git a/packages/router/src/experimental/route-resolver/matcher-resolve.spec.ts b/packages/router/src/experimental/route-resolver/matcher-resolve.spec.ts index 79d93813..2adbdb8a 100644 --- a/packages/router/src/experimental/route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/experimental/route-resolver/matcher-resolve.spec.ts @@ -1,33 +1,34 @@ import { describe, expect, it } from 'vitest' import { defineComponent } from 'vue' -import { RouteComponent, RouteMeta, RouteRecordRaw } from '../types' -import { NEW_stringifyURL } from '../location' -import { mockWarn } from '../../__tests__/vitest-mock-warn' +import { RouteComponent, RouteMeta, RouteRecordRaw } from '../../types' +import { NEW_stringifyURL } from '../../location' +import { mockWarn } from '../../../__tests__/vitest-mock-warn' import { - createCompiledMatcher, type MatcherLocationRaw, - type NEW_MatcherRecordRaw, - type NEW_LocationResolved, + type ResolverLocationResolved, type NEW_MatcherRecord, NO_MATCH_LOCATION, -} from './resolver' +} from './resolver-abstract' +import { type NEW_MatcherRecordRaw } from './resolver-dynamic' +import { createCompiledMatcher } from './resolver-dynamic' import { miss } from './matchers/errors' -import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern' -import { EXPERIMENTAL_RouterOptions } from '../experimental/router' -import { stringifyQuery } from '../query' -import type { - MatcherLocationAsNamed, - MatcherLocationAsPathAbsolute, -} from './matcher-location' +import { + MatcherPatternPath, + MatcherPatternPathStatic, +} from './matchers/matcher-pattern' +import { EXPERIMENTAL_RouterOptions } from '../router' +import { stringifyQuery } from '../../query' +import type { ResolverLocationAsPathAbsolute } from './resolver-abstract' +import type { ResolverLocationAsNamed } from './resolver-abstract' // TODO: should be moved to a different test file // used to check backward compatible paths import { PATH_PARSER_OPTIONS_DEFAULTS, PathParams, tokensToParser, -} from '../matcher/pathParserRanker' -import { tokenizePath } from '../matcher/pathTokenizer' -import { mergeOptions } from '../utils' +} from '../../matcher/pathParserRanker' +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 { @@ -128,7 +129,7 @@ describe('RouterMatcher.resolve', () => { function isMatcherLocationResolved( location: unknown - ): location is NEW_LocationResolved { + ): location is ResolverLocationResolved { return !!( location && typeof location === 'object' && @@ -155,11 +156,11 @@ describe('RouterMatcher.resolve', () => { toLocation: Exclude | `/${string}`, expectedLocation: Partial, fromLocation: - | NEW_LocationResolved + | ResolverLocationResolved // absolute locations only that can be resolved for convenience | `/${string}` - | MatcherLocationAsNamed - | MatcherLocationAsPathAbsolute = START_LOCATION + | ResolverLocationAsNamed + | ResolverLocationAsPathAbsolute = START_LOCATION ) { const records = (Array.isArray(record) ? record : [record]).map( (record): NEW_MatcherRecordRaw => diff --git a/packages/router/src/experimental/route-resolver/matcher-pattern.spec.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts similarity index 100% rename from packages/router/src/experimental/route-resolver/matcher-pattern.spec.ts rename to packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts diff --git a/packages/router/src/experimental/route-resolver/matcher-pattern.ts b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts similarity index 95% rename from packages/router/src/experimental/route-resolver/matcher-pattern.ts rename to packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts index 35117c8a..3bc66bce 100644 --- a/packages/router/src/experimental/route-resolver/matcher-pattern.ts +++ b/packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts @@ -1,6 +1,5 @@ -import { decode, MatcherQueryParams } from './resolver' -import { EmptyParams, MatcherParamsFormatted } from './matcher-location' -import { miss } from './matchers/errors' +import { decode, MatcherQueryParams } from '../resolver-abstract' +import { miss } from './errors' /** * Base interface for matcher patterns that extract params from a URL. @@ -274,4 +273,10 @@ export interface MatcherPatternQuery< */ export interface MatcherPatternHash< TParams extends MatcherParamsFormatted = MatcherParamsFormatted, -> extends MatcherPattern {} +> extends MatcherPattern {} /** + * Generic object of params that can be passed to a matcher. + */ +export type MatcherParamsFormatted = Record /** + * Empty object in TS. + */ +export type EmptyParams = Record diff --git a/packages/router/src/experimental/route-resolver/matchers/test-utils.ts b/packages/router/src/experimental/route-resolver/matchers/test-utils.ts index b48b9362..088c19a4 100644 --- a/packages/router/src/experimental/route-resolver/matchers/test-utils.ts +++ b/packages/router/src/experimental/route-resolver/matchers/test-utils.ts @@ -1,10 +1,10 @@ -import { EmptyParams } from '../matcher-location' +import { EmptyParams } from './matcher-pattern' import { MatcherPatternPath, MatcherPatternQuery, MatcherPatternHash, -} from '../matcher-pattern' -import { NEW_MatcherRecord } from '../resolver' +} from './matcher-pattern' +import { NEW_MatcherRecord } from '../resolver-abstract' import { invalid, miss } from './errors' export const ANY_PATH_PATTERN_MATCHER: MatcherPatternPath<{ diff --git a/packages/router/src/experimental/route-resolver/resolver-abstract.ts b/packages/router/src/experimental/route-resolver/resolver-abstract.ts new file mode 100644 index 00000000..4fc6707d --- /dev/null +++ b/packages/router/src/experimental/route-resolver/resolver-abstract.ts @@ -0,0 +1,284 @@ +import { type LocationQuery, type LocationQueryRaw } from '../../query' +import { warn } from '../../warning' +import { + encodeQueryValue as _encodeQueryValue, + encodeParam, +} from '../../encoding' +import type { MatcherParamsFormatted } from './matchers/matcher-pattern' +import { _RouteRecordProps } from '../../typed-routes' +import { NEW_MatcherDynamicRecord } from './resolver-dynamic' + +/** + * Allowed types for a matcher name. + */ +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 getRecords}. + */ +export interface NEW_RouterResolver_Base { + /** + * 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 + ): ResolverLocationResolved + + /** + * Resolves a string location relative to another location. A relative location can be `./same-folder`, + * `../parent-folder`, `same-folder`, or even `?page=2`. + */ + resolve( + relativeLocation: string, + currentLocation: ResolverLocationResolved + ): ResolverLocationResolved + + /** + * Resolves a location by its name. Any required params or query must be passed in the `options` argument. + */ + resolve( + location: ResolverLocationAsNamed, + // TODO: is this useful? + currentLocation?: undefined + // currentLocation?: undefined | NEW_LocationResolved + ): ResolverLocationResolved + + /** + * Resolves a location by its absolute path (starts with `/`). Any required query must be passed. + * @param location - The location to resolve. + */ + resolve( + location: ResolverLocationAsPathAbsolute, + // TODO: is this useful? + currentLocation?: undefined + // currentLocation?: NEW_LocationResolved | undefined + ): ResolverLocationResolved + + resolve( + location: ResolverLocationAsPathRelative, + currentLocation: ResolverLocationResolved + ): ResolverLocationResolved + + // NOTE: in practice, this overload can cause bugs. It's better to use named locations + + /** + * Resolves a location relative to another location. It reuses existing properties in the `currentLocation` like + * `params`, `query`, and `hash`. + */ + resolve( + relativeLocation: ResolverLocationAsRelative, + currentLocation: ResolverLocationResolved + ): ResolverLocationResolved + + /** + * 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 +} + +/** + * Allowed location objects to be passed to {@link NEW_RouterResolver['resolve']} + */ +export type MatcherLocationRaw = + // | `/${string}` + | string + | ResolverLocationAsNamed + | ResolverLocationAsPathAbsolute + | ResolverLocationAsPathRelative + | ResolverLocationAsRelative + +/** + * Returned location object by {@link NEW_RouterResolver['resolve']}. + * It contains the resolved name, params, query, hash, and matched records. + */ +export interface ResolverLocationResolved { + name: RecordName + params: MatcherParamsFormatted + + fullPath: string + path: string + query: LocationQuery + hash: string + + matched: TMatched[] +} + +export type MatcherPathParamsValue = string | null | string[] +/** + * Params in a string format so they can be encoded/decoded and put into a URL. + */ +export type MatcherPathParams = Record + +// TODO: move to matcher-pattern +export type MatcherQueryParamsValue = string | null | Array +export type MatcherQueryParams = Record + +/** + * Apply a function to all properties in an object. It's used to encode/decode params and queries. + * @internal + */ +export function applyFnToObject( + fn: (v: string | number | null | undefined) => R, + params: MatcherPathParams | LocationQuery | undefined +): Record { + const newParams: Record = {} + + for (const key in params) { + const value = params[key] + newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value) + } + + return newParams +} + +/** + * Decode text using `decodeURIComponent`. Returns the original text if it + * fails. + * + * @param text - string to decode + * @returns decoded string + */ +export function decode(text: string | number): string +export function decode(text: null | undefined): null +export function decode(text: string | number | null | undefined): string | null +export function decode( + text: string | number | null | undefined +): string | null { + if (text == null) return null + try { + return decodeURIComponent('' + text) + } catch (err) { + __DEV__ && warn(`Error decoding "${text}". Using original value`) + } + return '' + text +} +// TODO: just add the null check to the original function in encoding.ts + +interface FnStableNull { + (value: null | undefined): null + (value: string | number): string + // needed for the general case and must be last + (value: string | number | null | undefined): string | null +} + +// function encodeParam(text: null | undefined, encodeSlash?: boolean): null +// function encodeParam(text: string | number, encodeSlash?: boolean): string +// function encodeParam( +// text: string | number | null | undefined, +// encodeSlash?: boolean +// ): string | null +// function encodeParam( +// text: string | number | null | undefined, +// encodeSlash = true +// ): string | null { +// if (text == null) return null +// text = encodePath(text) +// return encodeSlash ? text.replace(SLASH_RE, '%2F') : text +// } + +// @ts-expect-error: overload are not correctly identified +const encodeQueryValue: FnStableNull = + // for ts + value => (value == null ? null : _encodeQueryValue(value)) + +// // @ts-expect-error: overload are not correctly identified +// const encodeQueryKey: FnStableNull = +// // 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< + ResolverLocationResolved, + 'path' | 'hash' | 'query' | 'fullPath' +> + +/** + * Normalized version of a {@link NEW_MatcherRecordRaw} record. + */ +export interface NEW_MatcherRecord extends NEW_MatcherDynamicRecord {} + +// FIXME: move somewhere else +/** + * Tagged template helper to encode params into a path. Doesn't work with null + */ +export function pathEncoded( + parts: TemplateStringsArray, + ...params: Array +): string { + return parts.reduce((result, part, i) => { + return ( + result + + part + + (Array.isArray(params[i]) + ? params[i].map(encodeParam).join('/') + : encodeParam(params[i])) + ) + }) +} +export interface ResolverLocationAsNamed { + name: RecordName + // FIXME: should this be optional? + params: MatcherParamsFormatted + query?: LocationQueryRaw + hash?: string + + /** + * @deprecated This is ignored when `name` is provided + */ + path?: undefined +} +export interface ResolverLocationAsPathRelative { + path: string + query?: LocationQueryRaw + hash?: string + + /** + * @deprecated This is ignored when `path` is provided + */ + name?: undefined + /** + * @deprecated This is ignored when `path` (instead of `name`) is provided + */ + params?: undefined +} // TODO: does it make sense to support absolute paths objects? + +export interface ResolverLocationAsPathAbsolute + extends ResolverLocationAsPathRelative { + path: `/${string}` +} +export interface ResolverLocationAsRelative { + params?: MatcherParamsFormatted + query?: LocationQueryRaw + hash?: string + + /** + * @deprecated This location is relative to the next parameter. This `name` will be ignored. + */ + name?: undefined + /** + * @deprecated This location is relative to the next parameter. This `path` will be ignored. + */ + path?: undefined +} diff --git a/packages/router/src/experimental/route-resolver/resolver.ts b/packages/router/src/experimental/route-resolver/resolver-dynamic.ts similarity index 59% rename from packages/router/src/experimental/route-resolver/resolver.ts rename to packages/router/src/experimental/route-resolver/resolver-dynamic.ts index f6fca8e5..7a1d0baa 100644 --- a/packages/router/src/experimental/route-resolver/resolver.ts +++ b/packages/router/src/experimental/route-resolver/resolver-dynamic.ts @@ -1,40 +1,29 @@ import { - type LocationQuery, - normalizeQuery, - parseQuery, - stringifyQuery, -} from '../../query' -import type { - MatcherPattern, - MatcherPatternHash, - MatcherPatternPath, - MatcherPatternQuery, -} from './matcher-pattern' -import { warn } from '../../warning' -import { - encodeQueryValue as _encodeQueryValue, - encodeParam, -} from '../../encoding' -import { - LocationNormalized, NEW_stringifyURL, + LocationNormalized, parseURL, resolveRelativePath, -} from '../../location' +} from 'src/location' +import { normalizeQuery, stringifyQuery, parseQuery } from 'src/query' +import type { MatcherParamsFormatted } from './matchers/matcher-pattern' +import type { ResolverLocationAsRelative } from './resolver-abstract' +import type { ResolverLocationAsPathAbsolute } from './resolver-abstract' +import type { ResolverLocationAsPathRelative } from './resolver-abstract' +import type { ResolverLocationAsNamed } from './resolver-abstract' +import { + MatcherQueryParams, + NEW_RouterResolver_Base, + NO_MATCH_LOCATION, + RecordName, + ResolverLocationResolved, +} from './resolver-abstract' +import { comparePathParserScore } from 'src/matcher/pathParserRanker' +import { warn } from 'src/warning' import type { - MatcherLocationAsNamed, - MatcherLocationAsPathAbsolute, - MatcherLocationAsPathRelative, - MatcherLocationAsRelative, - MatcherParamsFormatted, -} from './matcher-location' -import { _RouteRecordProps } from '../../typed-routes' -import { comparePathParserScore } from '../../matcher/pathParserRanker' - -/** - * Allowed types for a matcher name. - */ -export type RecordName = string | symbol + MatcherPatternPath, + MatcherPatternQuery, + MatcherPatternHash, +} from './matchers/matcher-pattern' /** * Manage and resolve routes. Also handles the encoding, decoding, parsing and @@ -43,84 +32,7 @@ export type RecordName = string | symbol * - `TMatcherRecordRaw` represents the raw record type passed to {@link addMatcher}. * - `TMatcherRecord` represents the normalized record type returned by {@link getRecords}. */ -export interface NEW_RouterResolver_Base { - /** - * 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 - /** - * Resolves a string location relative to another location. A relative location can be `./same-folder`, - * `../parent-folder`, `same-folder`, or even `?page=2`. - */ - resolve( - relativeLocation: string, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved - - /** - * Resolves a location by its name. Any required params or query must be passed in the `options` argument. - */ - resolve( - location: MatcherLocationAsNamed, - // TODO: is this useful? - currentLocation?: undefined - // currentLocation?: undefined | NEW_LocationResolved - ): NEW_LocationResolved - - /** - * Resolves a location by its absolute path (starts with `/`). Any required query must be passed. - * @param location - The location to resolve. - */ - resolve( - location: MatcherLocationAsPathAbsolute, - // TODO: is this useful? - currentLocation?: undefined - // currentLocation?: NEW_LocationResolved | undefined - ): NEW_LocationResolved - - resolve( - location: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved - - // NOTE: in practice, this overload can cause bugs. It's better to use named locations - - /** - * Resolves a location relative to another location. It reuses existing properties in the `currentLocation` like - * `params`, `query`, and `hash`. - */ - resolve( - relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved - ): NEW_LocationResolved - - /** - * 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 extends NEW_RouterResolver_Base { /** @@ -144,254 +56,6 @@ export interface NEW_RouterResolver */ clearMatchers(): void } - -/** - * Allowed location objects to be passed to {@link NEW_RouterResolver['resolve']} - */ -export type MatcherLocationRaw = - // | `/${string}` - | string - | MatcherLocationAsNamed - | MatcherLocationAsPathAbsolute - | MatcherLocationAsPathRelative - | MatcherLocationAsRelative - -// TODO: ResolverLocationResolved -export interface NEW_LocationResolved { - name: RecordName - params: MatcherParamsFormatted - - fullPath: string - path: string - query: LocationQuery - hash: string - - matched: TMatched[] -} - -export type MatcherPathParamsValue = string | null | string[] -/** - * Params in a string format so they can be encoded/decoded and put into a URL. - */ -export type MatcherPathParams = Record - -// TODO: move to matcher-pattern -export type MatcherQueryParamsValue = string | null | Array -export type MatcherQueryParams = Record - -/** - * Apply a function to all properties in an object. It's used to encode/decode params and queries. - * @internal - */ -export function applyFnToObject( - fn: (v: string | number | null | undefined) => R, - params: MatcherPathParams | LocationQuery | undefined -): Record { - const newParams: Record = {} - - for (const key in params) { - const value = params[key] - newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value) - } - - return newParams -} - -/** - * Decode text using `decodeURIComponent`. Returns the original text if it - * fails. - * - * @param text - string to decode - * @returns decoded string - */ -export function decode(text: string | number): string -export function decode(text: null | undefined): null -export function decode(text: string | number | null | undefined): string | null -export function decode( - text: string | number | null | undefined -): string | null { - if (text == null) return null - try { - return decodeURIComponent('' + text) - } catch (err) { - __DEV__ && warn(`Error decoding "${text}". Using original value`) - } - return '' + text -} -// TODO: just add the null check to the original function in encoding.ts - -interface FnStableNull { - (value: null | undefined): null - (value: string | number): string - // needed for the general case and must be last - (value: string | number | null | undefined): string | null -} - -// function encodeParam(text: null | undefined, encodeSlash?: boolean): null -// function encodeParam(text: string | number, encodeSlash?: boolean): string -// function encodeParam( -// text: string | number | null | undefined, -// encodeSlash?: boolean -// ): string | null -// function encodeParam( -// text: string | number | null | undefined, -// encodeSlash = true -// ): string | null { -// if (text == null) return null -// text = encodePath(text) -// return encodeSlash ? text.replace(SLASH_RE, '%2F') : text -// } - -// @ts-expect-error: overload are not correctly identified -const encodeQueryValue: FnStableNull = - // for ts - value => (value == null ? null : _encodeQueryValue(value)) - -// // @ts-expect-error: overload are not correctly identified -// const encodeQueryKey: FnStableNull = -// // 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' -> - -// FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc) - -/** - * Experimental 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?: RecordName - - /** - * Array of nested routes. - */ - children?: NEW_MatcherRecordRaw[] - - /** - * Is this a record that groups children. Cannot be matched - */ - group?: boolean - - score: Array -} - -export interface EXPERIMENTAL_ResolverRecord_Base { - /** - * Name of the matcher. Unique across all matchers. - */ - 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 - - // 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 -} - -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 -} - -/** - * Normalized version of a {@link NEW_MatcherRecordRaw} record. - */ -export interface NEW_MatcherRecord extends NEW_MatcherDynamicRecord {} - -/** - * Tagged template helper to encode params into a path. Doesn't work with null - */ -export function pathEncoded( - parts: TemplateStringsArray, - ...params: Array -): string { - return parts.reduce((result, part, i) => { - return ( - result + - part + - (Array.isArray(params[i]) - ? params[i].map(encodeParam).join('/') - : encodeParam(params[i])) - ) - }) -} - -// pathEncoded`/users/${1}` -// TODO: -// pathEncoded`/users/${null}/end` - -// const a: RouteRecordRaw = {} as any - -/** - * Build the `matched` array of a record that includes all parent records from the root to the current one. - */ -export function buildMatched( - record: T -): T[] { - const matched: T[] = [] - let node: T | undefined = record - while (node) { - matched.unshift(node) - node = node.parent - } - return matched -} - export function createCompiledMatcher< TMatcherRecord extends NEW_MatcherDynamicRecord, >( @@ -410,38 +74,37 @@ export function createCompiledMatcher< // encodeQueryValue // ) // const decodeQuery = transformObject.bind(null, decode, decode) - // NOTE: because of the overloads, we need to manually type the arguments type MatcherResolveArgs = | [absoluteLocation: `/${string}`, currentLocation?: undefined] | [ relativeLocation: string, - currentLocation: NEW_LocationResolved, + currentLocation: ResolverLocationResolved, ] | [ - absoluteLocation: MatcherLocationAsPathAbsolute, + absoluteLocation: ResolverLocationAsPathAbsolute, // Same as above // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined, ] | [ - relativeLocation: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved, + relativeLocation: ResolverLocationAsPathRelative, + currentLocation: ResolverLocationResolved, ] | [ - location: MatcherLocationAsNamed, + location: ResolverLocationAsNamed, // Same as above // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined, ] | [ - relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved, + relativeLocation: ResolverLocationAsRelative, + currentLocation: ResolverLocationResolved, ] function resolve( ...args: MatcherResolveArgs - ): NEW_LocationResolved { + ): ResolverLocationResolved { const [to, currentLocation] = args if (typeof to === 'object' && (to.name || to.path == null)) { @@ -518,7 +181,9 @@ export function createCompiledMatcher< } let matcher: TMatcherRecord | undefined - let matched: NEW_LocationResolved['matched'] | undefined + let matched: + | ResolverLocationResolved['matched'] + | undefined let parsedParams: MatcherParamsFormatted | null | undefined for (matcher of matchers) { @@ -538,7 +203,6 @@ export function createCompiledMatcher< // for (const matcher of matched) { // Object.assign(queryParams, matcher.query?.match(url.query)) // } - parsedParams = { ...pathParams, ...queryParams, ...hashParams } // we found our match! break @@ -633,9 +297,7 @@ export function createCompiledMatcher< getRecord, getRecords, } -} - -/** +} /** * Performs a binary search to find the correct insertion index for a new matcher. * * Matchers are primarily sorted by their score. If scores are tied then we also consider parent/child relationships, @@ -644,7 +306,8 @@ export function createCompiledMatcher< * @param matcher - new matcher to be inserted * @param matchers - existing matchers */ -function findInsertionIndex( + +export function findInsertionIndex( matcher: T, matchers: T[] ) { @@ -680,8 +343,9 @@ function findInsertionIndex( return upper } - -function getInsertionAncestor(matcher: T) { +export function getInsertionAncestor( + matcher: T +) { let ancestor: T | undefined = matcher while ((ancestor = ancestor.parent)) { @@ -691,13 +355,12 @@ function getInsertionAncestor(matcher: T) { } return -} - -/** +} /** * Checks if a record or any of its parent is an alias * @param record */ -function isAliasRecord( + +export function isAliasRecord( record: T | undefined ): boolean { while (record) { @@ -706,4 +369,99 @@ function isAliasRecord( } return false +} // pathEncoded`/users/${1}` +// TODO: +// pathEncoded`/users/${null}/end` +// const a: RouteRecordRaw = {} as any +/** + * Build the `matched` array of a record that includes all parent records from the root to the current one. + */ + +export function buildMatched( + record: T +): T[] { + const matched: T[] = [] + let node: T | undefined = record + while (node) { + matched.unshift(node) + node = node.parent + } + return matched +} +export interface EXPERIMENTAL_ResolverRecord_Base { + /** + * Name of the matcher. Unique across all matchers. + */ + 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 + + // TODO: here or in router + // redirect?: RouteRecordRedirectOption + parent?: this + // FIXME: this property is only needed for dynamic routing + children: this[] + aliasOf?: this + + /** + * Is this a record that groups children. Cannot be matched + */ + group?: boolean +} +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 +} // FIXME: later on, the MatcherRecord should be compatible with RouteRecordRaw (which can miss a path, have children, etc) +/** + * Experimental 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?: RecordName + + /** + * Array of nested routes. + */ + children?: NEW_MatcherRecordRaw[] + + /** + * Is this a record that groups children. Cannot be matched + */ + group?: boolean + + score: Array } diff --git a/packages/router/src/experimental/route-resolver/resolver-static.spec.ts b/packages/router/src/experimental/route-resolver/resolver-static.spec.ts index 6f46abb3..2ca07dfe 100644 --- a/packages/router/src/experimental/route-resolver/resolver-static.spec.ts +++ b/packages/router/src/experimental/route-resolver/resolver-static.spec.ts @@ -1,10 +1,10 @@ import { describe, expect, it } from 'vitest' import { createStaticResolver } from './resolver-static' -import { MatcherQueryParams, NO_MATCH_LOCATION } from './resolver' +import { MatcherQueryParams, NO_MATCH_LOCATION } from './resolver-abstract' import { MatcherPatternQuery, MatcherPatternPathStatic, -} from './matcher-pattern' +} from './matchers/matcher-pattern' import { EMPTY_PATH_PATTERN_MATCHER, USER_ID_PATH_PATTERN_MATCHER, diff --git a/packages/router/src/experimental/route-resolver/resolver-static.ts b/packages/router/src/experimental/route-resolver/resolver-static.ts index 0150aa90..acc10697 100644 --- a/packages/router/src/experimental/route-resolver/resolver-static.ts +++ b/packages/router/src/experimental/route-resolver/resolver-static.ts @@ -5,25 +5,23 @@ import { parseURL, resolveRelativePath, } from '../../location' -import { - MatcherLocationAsNamed, - MatcherLocationAsPathAbsolute, - MatcherLocationAsPathRelative, - MatcherLocationAsRelative, - MatcherParamsFormatted, -} from './matcher-location' +import { MatcherParamsFormatted } from './matchers/matcher-pattern' +import { ResolverLocationAsRelative } from './resolver-abstract' +import { ResolverLocationAsPathAbsolute } from './resolver-abstract' +import { ResolverLocationAsPathRelative } from './resolver-abstract' +import { ResolverLocationAsNamed } from './resolver-abstract' import { RecordName, MatcherQueryParams, - NEW_LocationResolved, + ResolverLocationResolved, NEW_RouterResolver_Base, NO_MATCH_LOCATION, -} from './resolver' +} from './resolver-abstract' import type { MatcherPatternPath, MatcherPatternQuery, MatcherPatternHash, -} from './matcher-pattern' +} from './matchers/matcher-pattern' // TODO: find a better name than static that doesn't conflict with static params // maybe fixed or simple @@ -53,7 +51,7 @@ export interface EXPERIMENTAL_ResolverRecord_Base { // TODO: here or in router // redirect?: RouteRecordRedirectOption - parent?: EXPERIMENTAL_ResolverRecord | null // the parent can be matchable or not + parent?: EXPERIMENTAL_ResolverRecord | null // the parend can be matchable or not // TODO: implement aliases // aliasOf?: this } @@ -120,31 +118,34 @@ export function createStaticResolver< // NOTE: because of the overloads for `resolve`, we need to manually type the arguments type _resolveArgs = | [absoluteLocation: `/${string}`, currentLocation?: undefined] - | [relativeLocation: string, currentLocation: NEW_LocationResolved] | [ - absoluteLocation: MatcherLocationAsPathAbsolute, + relativeLocation: string, + currentLocation: ResolverLocationResolved, + ] + | [ + absoluteLocation: ResolverLocationAsPathAbsolute, // Same as above // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined, ] | [ - relativeLocation: MatcherLocationAsPathRelative, - currentLocation: NEW_LocationResolved, + relativeLocation: ResolverLocationAsPathRelative, + currentLocation: ResolverLocationResolved, ] | [ - location: MatcherLocationAsNamed, + location: ResolverLocationAsNamed, // Same as above // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined, ] | [ - relativeLocation: MatcherLocationAsRelative, - currentLocation: NEW_LocationResolved, + relativeLocation: ResolverLocationAsRelative, + currentLocation: ResolverLocationResolved, ] function resolve( ...[to, currentLocation]: _resolveArgs - ): NEW_LocationResolved { + ): ResolverLocationResolved { if (typeof to === 'object' && (to.name || to.path == null)) { // relative location by path or by name if (__DEV__ && to.name == null && currentLocation == null) { @@ -218,7 +219,7 @@ export function createStaticResolver< } let record: TRecord | undefined - let matched: NEW_LocationResolved['matched'] | undefined + let matched: ResolverLocationResolved['matched'] | undefined let parsedParams: MatcherParamsFormatted | null | undefined for (record of records) { diff --git a/packages/router/src/experimental/route-resolver/resolver.spec.ts b/packages/router/src/experimental/route-resolver/resolver.spec.ts index 93a269c9..fea7655f 100644 --- a/packages/router/src/experimental/route-resolver/resolver.spec.ts +++ b/packages/router/src/experimental/route-resolver/resolver.spec.ts @@ -1,14 +1,11 @@ import { describe, expect, it } from 'vitest' -import { - createCompiledMatcher, - NO_MATCH_LOCATION, - pathEncoded, -} from './resolver' +import { NO_MATCH_LOCATION, pathEncoded } from './resolver-abstract' +import { createCompiledMatcher } from './resolver-dynamic' import { MatcherPatternQuery, MatcherPatternPathStatic, MatcherPatternPathDynamic, -} from './matcher-pattern' +} from './matchers/matcher-pattern' import { EMPTY_PATH_ROUTE, USER_ID_ROUTE, diff --git a/packages/router/src/experimental/route-resolver/resolver.test-d.ts b/packages/router/src/experimental/route-resolver/resolver.test-d.ts index 6da64da5..29717e8e 100644 --- a/packages/router/src/experimental/route-resolver/resolver.test-d.ts +++ b/packages/router/src/experimental/route-resolver/resolver.test-d.ts @@ -1,10 +1,8 @@ import { describe, expectTypeOf, it } from 'vitest' -import { - NEW_LocationResolved, - NEW_MatcherRecordRaw, - NEW_RouterResolver, -} from './resolver' -import { EXPERIMENTAL_RouteRecordNormalized } from '../experimental/router' +import { ResolverLocationResolved } from './resolver-abstract' +import { NEW_MatcherRecordRaw } from './resolver-dynamic' +import { NEW_RouterResolver } from './resolver-dynamic' +import { EXPERIMENTAL_RouteRecordNormalized } from '../router' describe('Matcher', () => { type TMatcherRecordRaw = NEW_MatcherRecordRaw @@ -16,10 +14,10 @@ describe('Matcher', () => { describe('matcher.resolve()', () => { it('resolves absolute string locations', () => { expectTypeOf(matcher.resolve({ path: '/foo' })).toEqualTypeOf< - NEW_LocationResolved + ResolverLocationResolved >() expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf< - NEW_LocationResolved + ResolverLocationResolved >() }) @@ -34,17 +32,17 @@ describe('Matcher', () => { expectTypeOf( matcher.resolve( { path: 'foo' }, - {} as NEW_LocationResolved + {} as ResolverLocationResolved ) - ).toEqualTypeOf>() + ).toEqualTypeOf>() expectTypeOf( - matcher.resolve('foo', {} as NEW_LocationResolved) - ).toEqualTypeOf>() + matcher.resolve('foo', {} as ResolverLocationResolved) + ).toEqualTypeOf>() }) it('resolved named locations', () => { expectTypeOf(matcher.resolve({ name: 'foo', params: {} })).toEqualTypeOf< - NEW_LocationResolved + ResolverLocationResolved >() }) @@ -59,9 +57,9 @@ describe('Matcher', () => { expectTypeOf( matcher.resolve( { params: { id: 1 } }, - {} as NEW_LocationResolved + {} as ResolverLocationResolved ) - ).toEqualTypeOf>() + ).toEqualTypeOf>() }) }) @@ -77,7 +75,7 @@ describe('Matcher', () => { // @ts-expect-error: name + currentLocation { name: 'a', params: {} }, // - {} as NEW_LocationResolved + {} as ResolverLocationResolved ) }) }) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 2a7c7606..9b2c5d78 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -84,6 +84,7 @@ import { EXPERIMENTAL_ResolverRecord_Matchable, EXPERIMENTAL_ResolverStatic, } from './route-resolver/resolver-static' +import { ResolverLocationResolved } from './route-resolver/resolver-abstract' /** * resolve, reject arguments of Promise constructor