]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: support partial locations
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 8 Jan 2025 16:50:23 +0000 (17:50 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 8 Jan 2025 16:50:23 +0000 (17:50 +0100)
packages/router/src/experimental/router.ts
packages/router/src/location.ts
packages/router/src/new-route-resolver/matcher-location.ts
packages/router/src/new-route-resolver/matcher-resolve.spec.ts
packages/router/src/new-route-resolver/matcher.spec.ts
packages/router/src/new-route-resolver/matcher.test-d.ts
packages/router/src/new-route-resolver/resolver.ts
packages/router/src/query.ts

index 32f8a0f0f20708e65fe618a79e285bf8d6ff673b..1b252336e3d620e1ef32fa605e984429279294b4 100644 (file)
@@ -83,6 +83,7 @@ import {
   routerKey,
   routerViewLocationKey,
 } from '../injectionSymbols'
+import { MatcherLocationAsPathAbsolute } from '../new-route-resolver/matcher-location'
 
 /**
  * resolve, reject arguments of Promise constructor
@@ -406,6 +407,11 @@ export interface EXPERIMENTAL_RouteRecordRaw extends NEW_MatcherRecordRaw {
    * Arbitrary data attached to the record.
    */
   meta?: RouteMeta
+
+  components?: Record<string, unknown>
+  component?: unknown
+
+  redirect?: unknown
 }
 
 // TODO: is it worth to have 2 types for the undefined values?
@@ -510,6 +516,15 @@ export function experimental_createRouter(
     return !!matcher.getMatcher(name)
   }
 
+  function locationAsObject(
+    to: RouteLocationRaw | RouteLocationNormalized,
+    currentLocation: string = currentRoute.value.path
+  ): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
+    return typeof to === 'string'
+      ? parseURL(parseQuery, to, currentLocation)
+      : to
+  }
+
   function resolve(
     rawLocation: RouteLocationRaw,
     currentLocation?: RouteLocationNormalizedLoaded
@@ -522,6 +537,11 @@ export function experimental_createRouter(
       currentLocation && assign({}, currentLocation || currentRoute.value)
     // currentLocation = assign({}, currentLocation || currentRoute.value)
 
+    const locationObject = locationAsObject(
+      rawLocation,
+      currentRoute.value.path
+    )
+
     if (__DEV__) {
       if (!isRouteLocation(rawLocation)) {
         warn(
@@ -531,12 +551,9 @@ export function experimental_createRouter(
         return resolve({})
       }
 
-      if (
-        typeof rawLocation === 'object' &&
-        !rawLocation.hash?.startsWith('#')
-      ) {
+      if (!locationObject.hash?.startsWith('#')) {
         warn(
-          `A \`hash\` should always start with the character "#". Replace "${rawLocation.hash}" with "#${rawLocation.hash}".`
+          `A \`hash\` should always start with the character "#". Replace "${locationObject.hash}" with "#${locationObject.hash}".`
         )
       }
     }
@@ -555,16 +572,20 @@ export function experimental_createRouter(
 
     const matchedRoute = matcher.resolve(
       // FIXME: should be ok
-      // @ts-expect-error: too many overlads
-      rawLocation,
-      currentLocation
+      // locationObject as MatcherLocationAsPathRelative,
+      // locationObject as MatcherLocationAsRelative,
+      // locationObject as MatcherLocationAsName, // TODO: this one doesn't allow an undefined currentLocation, the other ones work
+      locationObject as MatcherLocationAsPathAbsolute,
+      currentLocation as unknown as NEW_LocationResolved<EXPERIMENTAL_RouteRecordNormalized>
     )
     const href = routerHistory.createHref(matchedRoute.fullPath)
 
     if (__DEV__) {
       if (href.startsWith('//')) {
         warn(
-          `Location "${rawLocation}" resolved to "${href}". A resolved location cannot start with multiple slashes.`
+          `Location ${JSON.stringify(
+            rawLocation
+          )} resolved to "${href}". A resolved location cannot start with multiple slashes.`
         )
       }
       if (!matchedRoute.matched.length) {
@@ -581,14 +602,6 @@ export function experimental_createRouter(
     })
   }
 
-  function locationAsObject(
-    to: RouteLocationRaw | RouteLocationNormalized
-  ): Exclude<RouteLocationRaw, string> | RouteLocationNormalized {
-    return typeof to === 'string'
-      ? parseURL(parseQuery, to, currentRoute.value.path)
-      : assign({}, to)
-  }
-
   function checkCanceledNavigation(
     to: RouteLocationNormalized,
     from: RouteLocationNormalized
index 163bf6f8dd5ab03b71be4475f9db20948746c5ce..8ab2a18152d6586c25c012c35daf547ef8bb1709 100644 (file)
@@ -77,8 +77,8 @@ export function parseURL(
     hash = location.slice(hashPos, location.length)
   }
 
-  // TODO(major): path ?? location
   path = resolveRelativePath(
+    // TODO(major): path ?? location
     path != null
       ? path
       : // empty path means a relative query or hash `?foo=f`, `#thing`
index f597df07ff49dfe7c764da4179231c501c765300..e05fdf7b32389875774512040a3a8aa4e5d6cf41 100644 (file)
@@ -38,6 +38,9 @@ export interface MatcherLocationAsPathRelative {
    */
   params?: undefined
 }
+
+// TODO: does it make sense to support absolute paths objects?
+
 export interface MatcherLocationAsPathAbsolute
   extends MatcherLocationAsPathRelative {
   path: `/${string}`
index 6ad88939435ddeef62b76efd180e5661db5c3f31..a73a0584125f9f78278263127594b92f65fcb28e 100644 (file)
@@ -1,7 +1,7 @@
 import { describe, expect, it } from 'vitest'
 import { defineComponent } from 'vue'
 import { RouteComponent, RouteRecordRaw } from '../types'
-import { stringifyURL } from '../location'
+import { NEW_stringifyURL } from '../location'
 import { mockWarn } from '../../__tests__/vitest-mock-warn'
 import {
   createCompiledMatcher,
@@ -9,6 +9,7 @@ import {
   type NEW_MatcherRecordRaw,
   type NEW_LocationResolved,
   type NEW_MatcherRecord,
+  NO_MATCH_LOCATION,
 } from './resolver'
 import { miss } from './matchers/errors'
 import { MatcherPatternPath, MatcherPatternPathStatic } from './matcher-pattern'
@@ -20,14 +21,27 @@ import type {
 } from './matcher-location'
 // TODO: should be moved to a different test file
 // used to check backward compatible paths
-import { PathParams, tokensToParser } from '../matcher/pathParserRanker'
+import {
+  PATH_PARSER_OPTIONS_DEFAULTS,
+  PathParams,
+  tokensToParser,
+} from '../matcher/pathParserRanker'
 import { tokenizePath } from '../matcher/pathTokenizer'
+import { mergeOptions } from '../utils'
 
 // for raw route record
 const component: RouteComponent = defineComponent({})
 // for normalized route records
 const components = { default: component }
 
+function isMatchable(record: RouteRecordRaw): boolean {
+  return !!(
+    record.name ||
+    (record.components && Object.keys(record.components).length) ||
+    record.redirect
+  )
+}
+
 function compileRouteRecord(
   record: RouteRecordRaw,
   parentRecord?: RouteRecordRaw
@@ -38,14 +52,15 @@ function compileRouteRecord(
     ? record.path
     : (parentRecord?.path || '') + record.path
   record.path = path
-  const parser = tokensToParser(tokenizePath(record.path), {
-    // start: true,
-    end: record.end,
-    sensitive: record.sensitive,
-    strict: record.strict,
-  })
+  const parser = tokensToParser(
+    tokenizePath(record.path),
+    mergeOptions(PATH_PARSER_OPTIONS_DEFAULTS, record)
+  )
+
+  // console.log({ record, parser })
 
   return {
+    group: !isMatchable(record),
     name: record.name,
 
     path: {
@@ -122,7 +137,7 @@ describe('RouterMatcher.resolve', () => {
     const records = (Array.isArray(record) ? record : [record]).map(
       (record): EXPERIMENTAL_RouteRecordRaw =>
         isExperimentalRouteRecordRaw(record)
-          ? record
+          ? { components, ...record }
           : compileRouteRecord(record)
     )
     const matcher = createCompiledMatcher<NEW_MatcherRecord>()
@@ -139,15 +154,17 @@ describe('RouterMatcher.resolve', () => {
       path,
       query: {},
       hash: '',
+      // by default we have a symbol on every route
       name: expect.any(Symbol) as symbol,
       // must non enumerable
       // matched: [],
       params: (typeof toLocation === 'object' && toLocation.params) || {},
-      fullPath: stringifyURL(stringifyQuery, {
-        path: expectedLocation.path || '/',
-        query: expectedLocation.query,
-        hash: expectedLocation.hash,
-      }),
+      fullPath: NEW_stringifyURL(
+        stringifyQuery,
+        expectedLocation.path || path || '/',
+        expectedLocation.query,
+        expectedLocation.hash
+      ),
       ...expectedLocation,
     }
 
@@ -161,43 +178,29 @@ describe('RouterMatcher.resolve', () => {
 
     const resolvedFrom = isMatcherLocationResolved(fromLocation)
       ? fromLocation
-      : // FIXME: is this a ts bug?
-        // @ts-expect-error
-        matcher.resolve(fromLocation)
+      : matcher.resolve(
+          // FIXME: is this a ts bug?
+          // @ts-expect-error
+          typeof fromLocation === 'string'
+            ? { path: fromLocation }
+            : fromLocation
+        )
+
+    // console.log({ toLocation, resolved, expectedLocation, resolvedFrom })
 
     expect(
       matcher.resolve(
-        // FIXME: WTF?
+        // FIXME: should work now
         // @ts-expect-error
-        toLocation,
-        resolvedFrom
+        typeof toLocation === 'string' ? { path: toLocation } : toLocation,
+        resolvedFrom === START_LOCATION ? undefined : resolvedFrom
       )
     ).toMatchObject({
       ...resolved,
     })
   }
 
-  /**
-   *
-   * @param record - Record or records we are testing the matcher against
-   * @param location - location we want to resolve against
-   * @param [start] Optional currentLocation used when resolving
-   * @returns error
-   */
-  function assertErrorMatch(
-    record: RouteRecordRaw | RouteRecordRaw[],
-    toLocation: Exclude<MatcherLocationRaw, string> | `/${string}`,
-    fromLocation:
-      | NEW_LocationResolved<NEW_MatcherRecord>
-      // absolute locations only
-      | `/${string}`
-      | MatcherLocationAsNamed
-      | MatcherLocationAsPathAbsolute = START_LOCATION
-  ) {
-    assertRecordMatch(record, toLocation, {}, fromLocation)
-  }
-
-  describe.skip('LocationAsPath', () => {
+  describe('LocationAsPath', () => {
     it('resolves a normal path', () => {
       assertRecordMatch({ path: '/', name: 'Home', components }, '/', {
         name: 'Home',
@@ -207,10 +210,14 @@ describe('RouterMatcher.resolve', () => {
     })
 
     it('resolves a normal path without name', () => {
+      assertRecordMatch({ path: '/', components }, '/', {
+        path: '/',
+        params: {},
+      })
       assertRecordMatch(
         { path: '/', components },
         { path: '/' },
-        { name: undefined, path: '/', params: {} }
+        { path: '/', params: {} }
       )
     })
 
@@ -258,7 +265,7 @@ describe('RouterMatcher.resolve', () => {
       assertRecordMatch(
         { path: '/users/:id/:other', components },
         { path: '/users/posva/hey' },
-        { name: undefined, params: { id: 'posva', other: 'hey' } }
+        { name: expect.any(Symbol), params: { id: 'posva', other: 'hey' } }
       )
     })
 
@@ -266,7 +273,7 @@ describe('RouterMatcher.resolve', () => {
       assertRecordMatch(
         { path: '/', components },
         { path: '/foo' },
-        { name: undefined, params: {}, path: '/foo', matched: [] }
+        { params: {}, path: '/foo', matched: [] }
       )
     })
 
@@ -274,7 +281,7 @@ describe('RouterMatcher.resolve', () => {
       assertRecordMatch(
         { path: '/home/', name: 'Home', components },
         { path: '/home/' },
-        { name: 'Home', path: '/home/', matched: expect.any(Array) }
+        { name: 'Home', path: '/home/' }
       )
     })
 
@@ -309,13 +316,13 @@ describe('RouterMatcher.resolve', () => {
         path: '/home/',
         name: 'Home',
         components,
-        options: { strict: true },
+        strict: true,
       }
-      assertErrorMatch(record, { path: '/home' })
+      assertRecordMatch(record, { path: '/home' }, NO_MATCH_LOCATION)
       assertRecordMatch(
         record,
         { path: '/home/' },
-        { name: 'Home', path: '/home/', matched: expect.any(Array) }
+        { name: 'Home', path: '/home/' }
       )
     })
 
@@ -324,14 +331,14 @@ describe('RouterMatcher.resolve', () => {
         path: '/home',
         name: 'Home',
         components,
-        options: { strict: true },
+        strict: true,
       }
       assertRecordMatch(
         record,
         { path: '/home' },
-        { name: 'Home', path: '/home', matched: expect.any(Array) }
+        { name: 'Home', path: '/home' }
       )
-      assertErrorMatch(record, { path: '/home/' })
+      assertRecordMatch(record, { path: '/home/' }, NO_MATCH_LOCATION)
     })
   })
 
@@ -358,12 +365,10 @@ describe('RouterMatcher.resolve', () => {
     })
 
     it('throws if the named route does not exists', () => {
-      expect(() =>
-        assertErrorMatch(
-          { path: '/', components },
-          { name: 'Home', params: {} }
-        )
-      ).toThrowError('Matcher "Home" not found')
+      const matcher = createCompiledMatcher([])
+      expect(() => matcher.resolve({ name: 'Home', params: {} })).toThrowError(
+        'Matcher "Home" not found'
+      )
     })
 
     it('merges params', () => {
@@ -375,8 +380,9 @@ describe('RouterMatcher.resolve', () => {
       )
     })
 
-    // TODO: new matcher no longer allows implicit param merging
-    it.todo('only keep existing params', () => {
+    // TODO: this test doesn't seem useful, it's the same as the test above
+    // maybe remove it?
+    it('only keep existing params', () => {
       assertRecordMatch(
         { path: '/:a/:b', name: 'p', components },
         { name: 'p', params: { b: 'b' } },
@@ -464,13 +470,13 @@ describe('RouterMatcher.resolve', () => {
     })
   })
 
-  describe.skip('LocationAsRelative', () => {
+  describe('LocationAsRelative', () => {
     // TODO: not sure where this warning should appear now
     it.todo('warns if a path isn not absolute', () => {
       const matcher = createCompiledMatcher([
         { path: new MatcherPatternPathStatic('/') },
       ])
-      matcher.resolve('two', matcher.resolve('/'))
+      matcher.resolve({ path: 'two' }, matcher.resolve({ path: '/' }))
       expect('received "two"').toHaveBeenWarned()
     })
 
@@ -492,7 +498,7 @@ describe('RouterMatcher.resolve', () => {
       assertRecordMatch(
         record,
         { params: { id: 'posva', role: 'admin' } },
-        { name: undefined, path: '/users/posva/m/admin' },
+        { path: '/users/posva/m/admin' },
         {
           path: '/users/ed/m/user',
           // params: { id: 'ed', role: 'user' },
@@ -549,7 +555,6 @@ describe('RouterMatcher.resolve', () => {
         record,
         {},
         {
-          name: undefined,
           path: '/users/ed/m/user',
           params: { id: 'ed', role: 'user' },
         },
@@ -605,41 +610,36 @@ describe('RouterMatcher.resolve', () => {
     })
 
     it('throws if the current named route does not exists', () => {
-      const record = { path: '/', components }
-      const start = {
-        name: 'home',
-        params: {},
-        path: '/',
-        matched: [record],
-      }
-      // the property should be non enumerable
-      Object.defineProperty(start, 'matched', { enumerable: false })
-      expect(
-        assertErrorMatch(
-          record,
-          { params: { a: 'foo' } },
+      const matcher = createCompiledMatcher([])
+      expect(() =>
+        matcher.resolve(
+          {},
           {
-            name: 'home',
+            name: 'ko',
             params: {},
-            // matched: start.matched.map(normalizeRouteRecord),
-            // meta: {},
+            fullPath: '/',
+            hash: '',
+            matched: [],
+            path: '/',
+            query: {},
           }
         )
-      ).toMatchSnapshot()
+      ).toThrowError('Matcher "ko" not found')
     })
 
     it('avoids records with children without a component nor name', () => {
-      assertErrorMatch(
+      assertRecordMatch(
         {
           path: '/articles',
           children: [{ path: ':id', components }],
         },
-        { path: '/articles' }
+        { path: '/articles' },
+        NO_MATCH_LOCATION
       )
     })
 
-    it('avoid deeply nested records with children without a component nor name', () => {
-      assertErrorMatch(
+    it('avoids deeply nested records with children without a component nor name', () => {
+      assertRecordMatch(
         {
           path: '/app',
           components,
@@ -650,7 +650,8 @@ describe('RouterMatcher.resolve', () => {
             },
           ],
         },
-        { path: '/articles' }
+        { path: '/articles' },
+        NO_MATCH_LOCATION
       )
     })
 
index 335ddb83d763182eda4a1b471c926979ac52f0a4..ecc2d5e39969183f99c73dfb03d467e2a14f1fc9 100644 (file)
@@ -92,7 +92,7 @@ describe('RouterMatcher', () => {
         { path: new MatcherPatternPathStatic('/users') },
       ])
 
-      expect(matcher.resolve('/')).toMatchObject({
+      expect(matcher.resolve({ path: '/' })).toMatchObject({
         fullPath: '/',
         path: '/',
         params: {},
@@ -100,7 +100,7 @@ describe('RouterMatcher', () => {
         hash: '',
       })
 
-      expect(matcher.resolve('/users')).toMatchObject({
+      expect(matcher.resolve({ path: '/users' })).toMatchObject({
         fullPath: '/users',
         path: '/users',
         params: {},
@@ -122,7 +122,7 @@ describe('RouterMatcher', () => {
         },
       ])
 
-      expect(matcher.resolve('/users/1')).toMatchObject({
+      expect(matcher.resolve({ path: '/users/1' })).toMatchObject({
         fullPath: '/users/1',
         path: '/users/1',
         params: { id: '1' },
@@ -157,11 +157,11 @@ describe('RouterMatcher', () => {
   })
 
   describe('resolve()', () => {
-    describe('absolute locations as strings', () => {
+    describe.todo('absolute locations as strings', () => {
       it('resolves string locations with no params', () => {
         const matcher = createCompiledMatcher([EMPTY_PATH_ROUTE])
 
-        expect(matcher.resolve('/?a=a&b=b#h')).toMatchObject({
+        expect(matcher.resolve({ path: '/?a=a&b=b#h' })).toMatchObject({
           path: '/',
           params: {},
           query: { a: 'a', b: 'b' },
@@ -171,7 +171,7 @@ describe('RouterMatcher', () => {
 
       it('resolves a not found string', () => {
         const matcher = createCompiledMatcher()
-        expect(matcher.resolve('/bar?q=1#hash')).toEqual({
+        expect(matcher.resolve({ path: '/bar?q=1#hash' })).toEqual({
           ...NO_MATCH_LOCATION,
           fullPath: '/bar?q=1#hash',
           path: '/bar',
@@ -184,13 +184,13 @@ describe('RouterMatcher', () => {
       it('resolves string locations with params', () => {
         const matcher = createCompiledMatcher([USER_ID_ROUTE])
 
-        expect(matcher.resolve('/users/1?a=a&b=b#h')).toMatchObject({
+        expect(matcher.resolve({ path: '/users/1?a=a&b=b#h' })).toMatchObject({
           path: '/users/1',
           params: { id: 1 },
           query: { a: 'a', b: 'b' },
           hash: '#h',
         })
-        expect(matcher.resolve('/users/54?a=a&b=b#h')).toMatchObject({
+        expect(matcher.resolve({ path: '/users/54?a=a&b=b#h' })).toMatchObject({
           path: '/users/54',
           params: { id: 54 },
           query: { a: 'a', b: 'b' },
@@ -206,7 +206,7 @@ describe('RouterMatcher', () => {
           },
         ])
 
-        expect(matcher.resolve('/foo?page=100&b=b#h')).toMatchObject({
+        expect(matcher.resolve({ path: '/foo?page=100&b=b#h' })).toMatchObject({
           params: { page: 100 },
           path: '/foo',
           query: {
@@ -225,7 +225,7 @@ describe('RouterMatcher', () => {
           },
         ])
 
-        expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({
+        expect(matcher.resolve({ path: '/foo?a=a&b=b#bar' })).toMatchObject({
           hash: '#bar',
           params: { hash: 'bar' },
           path: '/foo',
@@ -242,7 +242,9 @@ describe('RouterMatcher', () => {
           },
         ])
 
-        expect(matcher.resolve('/users/24?page=100#bar')).toMatchObject({
+        expect(
+          matcher.resolve({ path: '/users/24?page=100#bar' })
+        ).toMatchObject({
           params: { id: 24, page: 100, hash: 'bar' },
         })
       })
@@ -255,7 +257,10 @@ describe('RouterMatcher', () => {
         ])
 
         expect(
-          matcher.resolve('foo', matcher.resolve('/nested/'))
+          matcher.resolve(
+            { path: 'foo' },
+            matcher.resolve({ path: '/nested/' })
+          )
         ).toMatchObject({
           params: {},
           path: '/nested/foo',
@@ -263,7 +268,10 @@ describe('RouterMatcher', () => {
           hash: '',
         })
         expect(
-          matcher.resolve('../foo', matcher.resolve('/nested/'))
+          matcher.resolve(
+            { path: '../foo' },
+            matcher.resolve({ path: '/nested/' })
+          )
         ).toMatchObject({
           params: {},
           path: '/foo',
@@ -271,7 +279,10 @@ describe('RouterMatcher', () => {
           hash: '',
         })
         expect(
-          matcher.resolve('./foo', matcher.resolve('/nested/'))
+          matcher.resolve(
+            { path: './foo' },
+            matcher.resolve({ path: '/nested/' })
+          )
         ).toMatchObject({
           params: {},
           path: '/nested/foo',
@@ -317,7 +328,7 @@ describe('RouterMatcher', () => {
       const matcher = createCompiledMatcher([ANY_PATH_ROUTE])
       describe('decodes', () => {
         it('handles encoded string path', () => {
-          expect(matcher.resolve('/%23%2F%3F')).toMatchObject({
+          expect(matcher.resolve({ path: '/%23%2F%3F' })).toMatchObject({
             fullPath: '/%23%2F%3F',
             path: '/%23%2F%3F',
             query: {},
@@ -326,7 +337,9 @@ describe('RouterMatcher', () => {
           })
         })
 
-        it('decodes query from a string', () => {
+        // TODO: move to the router as the matcher dosen't handle a plain string
+        it.todo('decodes query from a string', () => {
+          // @ts-expect-error: does not suppor fullPath
           expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({
             path: '/foo',
             fullPath: '/foo?foo=%23%2F%3F',
@@ -334,7 +347,8 @@ describe('RouterMatcher', () => {
           })
         })
 
-        it('decodes hash from a string', () => {
+        it.todo('decodes hash from a string', () => {
+          // @ts-expect-error: does not suppor fullPath
           expect(matcher.resolve('/foo#%22')).toMatchObject({
             path: '/foo',
             fullPath: '/foo#%22',
index 26060c3a69f8df7f2fbef2f62464d2259e293e3d..c04dfad31251b687ef69efd182e72b65cdf108b3 100644 (file)
@@ -15,7 +15,7 @@ describe('Matcher', () => {
 
   describe('matcher.resolve()', () => {
     it('resolves absolute string locations', () => {
-      expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf<
+      expectTypeOf(matcher.resolve({ path: '/foo' })).toEqualTypeOf<
         NEW_LocationResolved<TMatcherRecord>
       >()
     })
@@ -27,7 +27,10 @@ describe('Matcher', () => {
 
     it('resolves relative locations', () => {
       expectTypeOf(
-        matcher.resolve('foo', {} as NEW_LocationResolved<TMatcherRecord>)
+        matcher.resolve(
+          { path: 'foo' },
+          {} as NEW_LocationResolved<TMatcherRecord>
+        )
       ).toEqualTypeOf<NEW_LocationResolved<TMatcherRecord>>()
     })
 
index 17ceecf02ffe0d14fed475136bb3f6c61647f0a2..93b235c79560aba7407fb9a2bba13ab401d273c5 100644 (file)
@@ -1,9 +1,4 @@
-import {
-  type LocationQuery,
-  parseQuery,
-  normalizeQuery,
-  stringifyQuery,
-} from '../query'
+import { type LocationQuery, normalizeQuery, stringifyQuery } from '../query'
 import type {
   MatcherPatternHash,
   MatcherPatternPath,
@@ -11,7 +6,7 @@ import type {
 } from './matcher-pattern'
 import { warn } from '../warning'
 import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding'
-import { parseURL, NEW_stringifyURL } from '../location'
+import { NEW_stringifyURL, resolveRelativePath } from '../location'
 import type {
   MatcherLocationAsNamed,
   MatcherLocationAsPathAbsolute,
@@ -37,25 +32,27 @@ export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord> {
   /**
    * Resolves an absolute location (like `/path/to/somewhere`).
    */
-  resolve(
-    absoluteLocation: `/${string}`,
-    currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
-  ): NEW_LocationResolved<TMatcherRecord>
+  // resolve(
+  //   absoluteLocation: `/${string}`,
+  //   currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
+  // ): NEW_LocationResolved<TMatcherRecord>
 
   /**
    * 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<TMatcherRecord>
-  ): NEW_LocationResolved<TMatcherRecord>
+  // resolve(
+  //   relativeLocation: string,
+  //   currentLocation: NEW_LocationResolved<TMatcherRecord>
+  // ): NEW_LocationResolved<TMatcherRecord>
 
   /**
    * Resolves a location by its name. Any required params or query must be passed in the `options` argument.
    */
   resolve(
-    location: MatcherLocationAsNamed
+    location: MatcherLocationAsNamed,
+    // TODO: is this useful?
+    currentLocation?: undefined
   ): NEW_LocationResolved<TMatcherRecord>
 
   /**
@@ -63,7 +60,10 @@ export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord> {
    * @param location - The location to resolve.
    */
   resolve(
-    location: MatcherLocationAsPathAbsolute
+    location: MatcherLocationAsPathAbsolute,
+    // TODO: is this useful?
+    currentLocation?: undefined
+    // currentLocation?: NEW_LocationResolved<TMatcherRecord>
   ): NEW_LocationResolved<TMatcherRecord>
 
   resolve(
@@ -120,8 +120,8 @@ export interface NEW_RouterResolver<TMatcherRecordRaw, TMatcherRecord> {
  * Allowed location objects to be passed to {@link NEW_RouterResolver['resolve']}
  */
 export type MatcherLocationRaw =
-  | `/${string}`
-  | string
+  // | `/${string}`
+  // | string
   | MatcherLocationAsNamed
   | MatcherLocationAsPathAbsolute
   | MatcherLocationAsPathRelative
@@ -270,6 +270,11 @@ export interface NEW_MatcherRecordRaw {
    * Array of nested routes.
    */
   children?: NEW_MatcherRecordRaw[]
+
+  /**
+   * Is this a record that groups children. Cannot be matched
+   */
+  group?: boolean
 }
 
 export interface NEW_MatcherRecordBase<T> {
@@ -282,6 +287,8 @@ export interface NEW_MatcherRecordBase<T> {
   query?: MatcherPatternQuery
   hash?: MatcherPatternHash
 
+  group?: boolean
+
   parent?: T
 }
 
@@ -348,20 +355,23 @@ export function createCompiledMatcher<
 
   // NOTE: because of the overloads, we need to manually type the arguments
   type MatcherResolveArgs =
+    // | [
+    //     absoluteLocation: `/${string}`,
+    //     currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
+    //   ]
+    // | [
+    //     relativeLocation: string,
+    //     currentLocation: NEW_LocationResolved<TMatcherRecord>
+    //   ]
     | [
-        absoluteLocation: `/${string}`,
-        currentLocation?: undefined | NEW_LocationResolved<TMatcherRecord>
-      ]
-    | [
-        relativeLocation: string,
-        currentLocation: NEW_LocationResolved<TMatcherRecord>
+        absoluteLocation: MatcherLocationAsPathAbsolute,
+        currentLocation?: undefined
       ]
-    | [absoluteLocation: MatcherLocationAsPathAbsolute]
     | [
         relativeLocation: MatcherLocationAsPathRelative,
         currentLocation: NEW_LocationResolved<TMatcherRecord>
       ]
-    | [location: MatcherLocationAsNamed]
+    | [location: MatcherLocationAsNamed, currentLocation?: undefined]
     | [
         relativeLocation: MatcherLocationAsRelative,
         currentLocation: NEW_LocationResolved<TMatcherRecord>
@@ -370,12 +380,76 @@ export function createCompiledMatcher<
   function resolve(
     ...args: MatcherResolveArgs
   ): NEW_LocationResolved<TMatcherRecord> {
-    const [location, currentLocation] = args
+    const [to, currentLocation] = args
+
+    if (to.name || to.path == null) {
+      // relative location or by name
+      if (__DEV__ && to.name == null && currentLocation == null) {
+        console.warn(
+          `Cannot resolve an unnamed relative location 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,
+        }
+      }
 
-    // string location, e.g. '/foo', '../bar', 'baz', '?page=1'
-    if (typeof location === 'string') {
+      // either one of them must be defined and is catched by the dev only warn above
+      const name = to.name ?? currentLocation?.name
+      // FIXME: remove once name cannot be null
+      const matcher = name != null && matchers.get(name)
+      if (!matcher) {
+        throw new Error(`Matcher "${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 = matcher.path.build(params)
+      const hash = matcher.hash?.build(params) ?? ''
+      const matched = buildMatched(matcher)
+      const query = Object.assign(
+        {
+          ...currentLocation?.query,
+          ...normalizeQuery(to.query),
+        },
+        ...matched.map(matcher => matcher.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
-      const url = parseURL(parseQuery, location, currentLocation?.path)
+      // parseURL(to.path, currentLocation?.path)
+      const query = normalizeQuery(to.query)
+      const url = {
+        fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash),
+        path: resolveRelativePath(to.path, currentLocation?.path || '/'),
+        query,
+        hash: to.hash || '',
+      }
 
       let matcher: TMatcherRecord | undefined
       let matched: NEW_LocationResolved<TMatcherRecord>['matched'] | undefined
@@ -412,8 +486,8 @@ export function createCompiledMatcher<
           ...url,
           ...NO_MATCH_LOCATION,
           // already decoded
-          query: url.query,
-          hash: url.hash,
+          // query: url.query,
+          // hash: url.hash,
         }
       }
 
@@ -422,68 +496,13 @@ export function createCompiledMatcher<
         // matcher exists if matched exists
         name: matcher!.name,
         params: parsedParams,
-        // already decoded
-        query: url.query,
-        hash: url.hash,
         matched,
       }
       // TODO: handle object location { path, query, hash }
-    } else {
-      // relative location or by name
-      if (__DEV__ && location.name == null && currentLocation == null) {
-        console.warn(
-          `Cannot resolve an unnamed relative location without a current location. This will throw in production.`,
-          location
-        )
-        const query = normalizeQuery(location.query)
-        const hash = location.hash ?? ''
-        const path = location.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 = location.name ?? currentLocation!.name
-      // FIXME: remove once name cannot be null
-      const matcher = name != null && matchers.get(name)
-      if (!matcher) {
-        throw new Error(`Matcher "${String(location.name)}" not found`)
-      }
-
-      // unencoded params in a formatted form that the user came up with
-      const params: MatcherParamsFormatted = {
-        ...currentLocation?.params,
-        ...location.params,
-      }
-      const path = matcher.path.build(params)
-      const hash = matcher.hash?.build(params) ?? ''
-      const matched = buildMatched(matcher)
-      const query = Object.assign(
-        {
-          ...currentLocation?.query,
-          ...normalizeQuery(location.query),
-        },
-        ...matched.map(matcher => matcher.query?.build(params))
-      )
-
-      return {
-        name,
-        fullPath: NEW_stringifyURL(stringifyQuery, path, query, hash),
-        path,
-        query,
-        hash,
-        params,
-        matched,
-      }
     }
   }
 
-  function addRoute(record: NEW_MatcherRecordRaw, parent?: TMatcherRecord) {
+  function addMatcher(record: NEW_MatcherRecordRaw, parent?: TMatcherRecord) {
     const name = record.name ?? (__DEV__ ? Symbol('unnamed-route') : Symbol())
     // FIXME: proper normalization of the record
     // @ts-expect-error: we are not properly normalizing the record yet
@@ -492,20 +511,24 @@ export function createCompiledMatcher<
       name,
       parent,
     }
-    matchers.set(name, normalizedRecord)
+    // TODO:
+    // record.children
+    if (!normalizedRecord.group) {
+      matchers.set(name, normalizedRecord)
+    }
     return normalizedRecord
   }
 
   for (const record of records) {
-    addRoute(record)
+    addMatcher(record)
   }
 
-  function removeRoute(matcher: TMatcherRecord) {
+  function removeMatcher(matcher: TMatcherRecord) {
     matchers.delete(matcher.name)
     // TODO: delete children and aliases
   }
 
-  function clearRoutes() {
+  function clearMatchers() {
     matchers.clear()
   }
 
@@ -520,9 +543,9 @@ export function createCompiledMatcher<
   return {
     resolve,
 
-    addMatcher: addRoute,
-    removeMatcher: removeRoute,
-    clearMatchers: clearRoutes,
+    addMatcher,
+    removeMatcher,
+    clearMatchers,
     getMatcher,
     getMatchers,
   }
index 55e77c714f3ee8efbfca874b6ce26fd50b30ec2e..79feb6e436939f81c787128a5aa3714cb28944be 100644 (file)
@@ -16,7 +16,7 @@ import { isArray } from './utils'
  */
 export type LocationQueryValue = string | null
 /**
- * Possible values when defining a query.
+ * Possible values when defining a query. `undefined` allows to remove a value.
  *
  * @internal
  */