]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor: reorg resolver
authorEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jul 2025 13:33:47 +0000 (15:33 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Fri, 25 Jul 2025 13:33:47 +0000 (15:33 +0200)
14 files changed:
packages/router/src/experimental/index.ts
packages/router/src/experimental/route-resolver/index.ts [deleted file]
packages/router/src/experimental/route-resolver/matcher-location.ts [deleted file]
packages/router/src/experimental/route-resolver/matcher-resolve.spec.ts
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.spec.ts [moved from packages/router/src/experimental/route-resolver/matcher-pattern.spec.ts with 100% similarity]
packages/router/src/experimental/route-resolver/matchers/matcher-pattern.ts [moved from packages/router/src/experimental/route-resolver/matcher-pattern.ts with 95% similarity]
packages/router/src/experimental/route-resolver/matchers/test-utils.ts
packages/router/src/experimental/route-resolver/resolver-abstract.ts [new file with mode: 0644]
packages/router/src/experimental/route-resolver/resolver-dynamic.ts [moved from packages/router/src/experimental/route-resolver/resolver.ts with 59% similarity]
packages/router/src/experimental/route-resolver/resolver-static.spec.ts
packages/router/src/experimental/route-resolver/resolver-static.ts
packages/router/src/experimental/route-resolver/resolver.spec.ts
packages/router/src/experimental/route-resolver/resolver.test-d.ts
packages/router/src/experimental/router.ts

index 8af1615a3647ac93644699bb70d1368f44383a9e..62ae84c6485904a8a29a3577016f80ff8c518c83 100644 (file)
@@ -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 (file)
index 4c07b32..0000000
+++ /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 (file)
index 8d955b7..0000000
+++ /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<string, unknown>
-
-/**
- * Empty object in TS.
- */
-export type EmptyParams = Record<PropertyKey, never>
-
-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
-}
index 79d93813ea381b2cdfd7e56a8eefd902001c2414..2adbdb8a03b4e6fd0b38fe7d324526f5bf088985 100644 (file)
@@ -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<NEW_MatcherRecord> {
+  ): location is ResolverLocationResolved<NEW_MatcherRecord> {
     return !!(
       location &&
       typeof location === 'object' &&
@@ -155,11 +156,11 @@ describe('RouterMatcher.resolve', () => {
     toLocation: Exclude<MatcherLocationRaw, string> | `/${string}`,
     expectedLocation: Partial<MatcherResolvedLocation>,
     fromLocation:
-      | NEW_LocationResolved<NEW_MatcherRecord>
+      | ResolverLocationResolved<NEW_MatcherRecord>
       // 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 =>
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 35117c8a6b587c59dea0b3e07a3bf15984d49091..3bc66bce5ba6b9cf177876513ad6303f68386121 100644 (file)
@@ -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<string, TParams> {}
+> extends MatcherPattern<string, TParams> {} /**
+ * Generic object of params that can be passed to a matcher.
+ */
+export type MatcherParamsFormatted = Record<string, unknown> /**
+ * Empty object in TS.
+ */
+export type EmptyParams = Record<PropertyKey, never>
index b48b9362ea8ef088aba5fa5ada7e5f1363277c3f..088c19a49dc93a9ed9fdfb98dc6b627613f4073a 100644 (file)
@@ -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 (file)
index 0000000..4fc6707
--- /dev/null
@@ -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<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
+  ): ResolverLocationResolved<TRecord>
+
+  /**
+   * 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<TRecord>
+  ): ResolverLocationResolved<TRecord>
+
+  /**
+   * 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<TMatcherRecord>
+  ): ResolverLocationResolved<TRecord>
+
+  /**
+   * 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<TMatcherRecord> | undefined
+  ): ResolverLocationResolved<TRecord>
+
+  resolve(
+    location: ResolverLocationAsPathRelative,
+    currentLocation: ResolverLocationResolved<TRecord>
+  ): ResolverLocationResolved<TRecord>
+
+  // 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<TRecord>
+  ): ResolverLocationResolved<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
+}
+
+/**
+ * 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<TMatched> {
+  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<string, MatcherPathParamsValue>
+
+// TODO: move to matcher-pattern
+export type MatcherQueryParamsValue = string | null | Array<string | null>
+export type MatcherQueryParams = Record<string, MatcherQueryParamsValue>
+
+/**
+ * Apply a function to all properties in an object. It's used to encode/decode params and queries.
+ * @internal
+ */
+export function applyFnToObject<R>(
+  fn: (v: string | number | null | undefined) => R,
+  params: MatcherPathParams | LocationQuery | undefined
+): Record<string, R | R[]> {
+  const newParams: Record<string, R | R[]> = {}
+
+  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<unknown>,
+  '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 | number | (string | number)[]>
+): 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
+}
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 f6fca8e58c38e8cd36880c8979ce651122237528..7a1d0baa74747e3bd577868671b7e7c9777cf6a3 100644 (file)
@@ -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<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<TRecord>
 
-  /**
-   * 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<TRecord>
-  ): NEW_LocationResolved<TRecord>
-
-  /**
-   * 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<TMatcherRecord>
-  ): NEW_LocationResolved<TRecord>
-
-  /**
-   * 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<TMatcherRecord> | undefined
-  ): NEW_LocationResolved<TRecord>
-
-  resolve(
-    location: MatcherLocationAsPathRelative,
-    currentLocation: NEW_LocationResolved<TRecord>
-  ): NEW_LocationResolved<TRecord>
-
-  // 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<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> {
   /**
@@ -144,254 +56,6 @@ export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord>
    */
   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<TMatched> {
-  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<string, MatcherPathParamsValue>
-
-// TODO: move to matcher-pattern
-export type MatcherQueryParamsValue = string | null | Array<string | null>
-export type MatcherQueryParams = Record<string, MatcherQueryParamsValue>
-
-/**
- * Apply a function to all properties in an object. It's used to encode/decode params and queries.
- * @internal
- */
-export function applyFnToObject<R>(
-  fn: (v: string | number | null | undefined) => R,
-  params: MatcherPathParams | LocationQuery | undefined
-): Record<string, R | R[]> {
-  const newParams: Record<string, R | R[]> = {}
-
-  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<unknown>,
-  '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<number[]>
-}
-
-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<number[]>
-}
-
-/**
- * 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 | number | (string | number)[]>
-): 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<T extends EXPERIMENTAL_ResolverRecord_Base>(
-  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<TMatcherRecord>,
+        currentLocation: ResolverLocationResolved<TMatcherRecord>,
       ]
     | [
-        absoluteLocation: MatcherLocationAsPathAbsolute,
+        absoluteLocation: ResolverLocationAsPathAbsolute,
         // Same as above
         // currentLocation?: NEW_LocationResolved<TMatcherRecord> | undefined
         currentLocation?: undefined,
       ]
     | [
-        relativeLocation: MatcherLocationAsPathRelative,
-        currentLocation: NEW_LocationResolved<TMatcherRecord>,
+        relativeLocation: ResolverLocationAsPathRelative,
+        currentLocation: ResolverLocationResolved<TMatcherRecord>,
       ]
     | [
-        location: MatcherLocationAsNamed,
+        location: ResolverLocationAsNamed,
         // Same as above
         // currentLocation?: NEW_LocationResolved<TMatcherRecord> | undefined
         currentLocation?: undefined,
       ]
     | [
-        relativeLocation: MatcherLocationAsRelative,
-        currentLocation: NEW_LocationResolved<TMatcherRecord>,
+        relativeLocation: ResolverLocationAsRelative,
+        currentLocation: ResolverLocationResolved<TMatcherRecord>,
       ]
 
   function resolve(
     ...args: MatcherResolveArgs
-  ): NEW_LocationResolved<TMatcherRecord> {
+  ): ResolverLocationResolved<TMatcherRecord> {
     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<TMatcherRecord>['matched'] | undefined
+      let matched:
+        | ResolverLocationResolved<TMatcherRecord>['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<T extends NEW_MatcherDynamicRecord>(
+
+export function findInsertionIndex<T extends NEW_MatcherDynamicRecord>(
   matcher: T,
   matchers: T[]
 ) {
@@ -680,8 +343,9 @@ function findInsertionIndex<T extends NEW_MatcherDynamicRecord>(
 
   return upper
 }
-
-function getInsertionAncestor<T extends NEW_MatcherDynamicRecord>(matcher: T) {
+export function getInsertionAncestor<T extends NEW_MatcherDynamicRecord>(
+  matcher: T
+) {
   let ancestor: T | undefined = matcher
 
   while ((ancestor = ancestor.parent)) {
@@ -691,13 +355,12 @@ function getInsertionAncestor<T extends NEW_MatcherDynamicRecord>(matcher: T) {
   }
 
   return
-}
-
-/**
+} /**
  * Checks if a record or any of its parent is an alias
  * @param record
  */
-function isAliasRecord<T extends NEW_MatcherDynamicRecord>(
+
+export function isAliasRecord<T extends NEW_MatcherDynamicRecord>(
   record: T | undefined
 ): boolean {
   while (record) {
@@ -706,4 +369,99 @@ function isAliasRecord<T extends NEW_MatcherDynamicRecord>(
   }
 
   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<T extends EXPERIMENTAL_ResolverRecord_Base>(
+  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<number[]>
+} // 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<number[]>
 }
index 6f46abb3eddd9f74b6f7a6a25f630ae572cf2be3..2ca07dfec94b0841414d06fca545dc837c51721f 100644 (file)
@@ -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,
index 0150aa90d755f1de7fabbb071c8aef00fe84723f..acc106974f8b3c63caeb1ceec1119435401381fd 100644 (file)
@@ -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<TRecord>]
     | [
-        absoluteLocation: MatcherLocationAsPathAbsolute,
+        relativeLocation: string,
+        currentLocation: ResolverLocationResolved<TRecord>,
+      ]
+    | [
+        absoluteLocation: ResolverLocationAsPathAbsolute,
         // Same as above
         // currentLocation?: NEW_LocationResolved<TRecord> | undefined
         currentLocation?: undefined,
       ]
     | [
-        relativeLocation: MatcherLocationAsPathRelative,
-        currentLocation: NEW_LocationResolved<TRecord>,
+        relativeLocation: ResolverLocationAsPathRelative,
+        currentLocation: ResolverLocationResolved<TRecord>,
       ]
     | [
-        location: MatcherLocationAsNamed,
+        location: ResolverLocationAsNamed,
         // Same as above
         // currentLocation?: NEW_LocationResolved<TRecord> | undefined
         currentLocation?: undefined,
       ]
     | [
-        relativeLocation: MatcherLocationAsRelative,
-        currentLocation: NEW_LocationResolved<TRecord>,
+        relativeLocation: ResolverLocationAsRelative,
+        currentLocation: ResolverLocationResolved<TRecord>,
       ]
 
   function resolve(
     ...[to, currentLocation]: _resolveArgs
-  ): NEW_LocationResolved<TRecord> {
+  ): ResolverLocationResolved<TRecord> {
     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<TRecord>['matched'] | undefined
+      let matched: ResolverLocationResolved<TRecord>['matched'] | undefined
       let parsedParams: MatcherParamsFormatted | null | undefined
 
       for (record of records) {
index 93a269c96d35c669ea22679a520df37d5138ed9e..fea7655f3c8c462ca74fd5e333d46ee04f1ceca3 100644 (file)
@@ -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,
index 6da64da5187fedf3136225cba12208f52f746149..29717e8e92418e27a9f952dd7f808ca2a298b291 100644 (file)
@@ -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<TMatcherRecord>
+        ResolverLocationResolved<TMatcherRecord>
       >()
       expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf<
-        NEW_LocationResolved<TMatcherRecord>
+        ResolverLocationResolved<TMatcherRecord>
       >()
     })
 
@@ -34,17 +32,17 @@ describe('Matcher', () => {
       expectTypeOf(
         matcher.resolve(
           { path: 'foo' },
-          {} as NEW_LocationResolved<TMatcherRecord>
+          {} as ResolverLocationResolved<TMatcherRecord>
         )
-      ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
+      ).toEqualTypeOf<ResolverLocationResolved<TMatcherRecord>>()
       expectTypeOf(
-        matcher.resolve('foo', {} as NEW_LocationResolved<TMatcherRecord>)
-      ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
+        matcher.resolve('foo', {} as ResolverLocationResolved<TMatcherRecord>)
+      ).toEqualTypeOf<ResolverLocationResolved<TMatcherRecord>>()
     })
 
     it('resolved named locations', () => {
       expectTypeOf(matcher.resolve({ name: 'foo', params: {} })).toEqualTypeOf<
-        NEW_LocationResolved<TMatcherRecord>
+        ResolverLocationResolved<TMatcherRecord>
       >()
     })
 
@@ -59,9 +57,9 @@ describe('Matcher', () => {
       expectTypeOf(
         matcher.resolve(
           { params: { id: 1 } },
-          {} as NEW_LocationResolved<TMatcherRecord>
+          {} as ResolverLocationResolved<TMatcherRecord>
         )
-      ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
+      ).toEqualTypeOf<ResolverLocationResolved<TMatcherRecord>>()
     })
   })
 
@@ -77,7 +75,7 @@ describe('Matcher', () => {
       // @ts-expect-error: name + currentLocation
       { name: 'a', params: {} },
       //
-      {} as NEW_LocationResolved<TMatcherRecord>
+      {} as ResolverLocationResolved<TMatcherRecord>
     )
   })
 })
index 2a7c7606e24bd8ea61dbc6d0eed582b8f1af520c..9b2c5d78fb0159c24555310ea77c55581f38565f 100644 (file)
@@ -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