]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
chore: build location
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 25 Jun 2024 15:22:01 +0000 (17:22 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 4 Dec 2024 15:10:34 +0000 (16:10 +0100)
packages/router/src/new-matcher/matcher-pattern.ts
packages/router/src/new-matcher/matcher.spec.ts
packages/router/src/new-matcher/matcher.ts

index 021b975c0ea167692f24efea84bc71e5ab2eb1b5..bb993658c390978e9a29b8fb4eb7be3c785ceb92 100644 (file)
@@ -13,12 +13,12 @@ export interface MatcherPattern {
   name: MatcherName
 
   /**
-   * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash. If any of them is missing, returns `null`. TODO: throw instead?
+   * Extracts from a formatted, unencoded params object the ones belonging to the path, query, and hash.
    * @param params - Params to extract from.
    */
   unformatParams(
     params: MatcherParamsFormatted
-  ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null]
+  ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string]
 
   /**
    * Extracts the defined params from an encoded path, query, and hash parsed from a URL. Does not apply formatting or
@@ -44,7 +44,7 @@ export interface MatcherPattern {
     path: string
     query: MatcherQueryParams
     hash: string
-  }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null]
+  }): [path: MatcherPathParams, query: MatcherQueryParams, hash: string]
 
   /**
    * Takes encoded params object to form the `path`,
@@ -59,7 +59,7 @@ export interface MatcherPattern {
   formatParams(
     path: MatcherPathParams,
     query: MatcherQueryParams,
-    hash: string | null
+    hash: string
   ): MatcherParamsFormatted
 }
 
@@ -82,13 +82,16 @@ export interface PatternHashParamOptions
   extends PatternParamOptions_Base<string> {}
 
 export interface MatcherPatternPath {
+  build(path: MatcherPathParams): string
   match(path: string): MatcherPathParams
   format(params: MatcherPathParams): MatcherParamsFormatted
+  unformat(params: MatcherParamsFormatted): MatcherPathParams
 }
 
 export interface MatcherPatternQuery {
   match(query: MatcherQueryParams): MatcherQueryParams
   format(params: MatcherQueryParams): MatcherParamsFormatted
+  unformat(params: MatcherParamsFormatted): MatcherQueryParams
 }
 
 export interface MatcherPatternHash {
@@ -98,6 +101,7 @@ export interface MatcherPatternHash {
    */
   match(hash: string): string
   format(hash: string): MatcherParamsFormatted
+  unformat(params: MatcherParamsFormatted): string
 }
 
 export class MatcherPatternImpl implements MatcherPattern {
@@ -133,12 +137,16 @@ export class MatcherPatternImpl implements MatcherPattern {
   }
 
   buildPath(path: MatcherPathParams): string {
-    return ''
+    return this.path.build(path)
   }
 
   unformatParams(
     params: MatcherParamsFormatted
-  ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string | null] {
-    throw new Error('Method not implemented.')
+  ): [path: MatcherPathParams, query: MatcherQueryParams, hash: string] {
+    return [
+      this.path.unformat(params),
+      this.query?.unformat(params) ?? {},
+      this.hash?.unformat(params) ?? '',
+    ]
   }
 }
index 29f6a40a3c9d5f68ceed17f79168ec65a31bf993..9c6ccb2fe48243beb244f0c139b7b0fb46b1a2d6 100644 (file)
@@ -11,93 +11,212 @@ function createMatcherPattern(
 const EMPTY_PATH_PATTERN_MATCHER = {
   match: (path: string) => ({}),
   format: (params: {}) => ({}),
+  unformat: (params: {}) => ({}),
+  build: () => '/',
 } satisfies MatcherPatternPath
 
 describe('Matcher', () => {
   describe('resolve()', () => {
-    it('resolves string locations with no params', () => {
-      const matcher = createCompiledMatcher()
-      matcher.addRoute(
-        createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER)
-      )
-
-      expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({
-        path: '/foo',
-        params: {},
-        query: { a: 'a', b: 'b' },
-        hash: '#h',
+    describe('absolute locationss as strings', () => {
+      it('resolves string locations with no params', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER)
+        )
+
+        expect(matcher.resolve('/foo?a=a&b=b#h')).toMatchObject({
+          path: '/foo',
+          params: {},
+          query: { a: 'a', b: 'b' },
+          hash: '#h',
+        })
       })
-    })
 
-    it('resolves string locations with params', () => {
-      const matcher = createCompiledMatcher()
-      matcher.addRoute(
-        // /users/:id
-        createMatcherPattern(Symbol('foo'), {
-          match: (path: string) => {
-            const match = path.match(/^\/foo\/([^/]+?)$/)
-            if (!match) throw new Error('no match')
-            return { id: match[1] }
+      it('resolves string locations with params', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          // /users/:id
+          createMatcherPattern(Symbol('foo'), {
+            match: (path: string) => {
+              const match = path.match(/^\/foo\/([^/]+?)$/)
+              if (!match) throw new Error('no match')
+              return { id: match[1] }
+            },
+            format: (params: { id: string }) => ({ id: Number(params.id) }),
+            unformat: (params: { id: number }) => ({ id: String(params.id) }),
+            build: params => `/foo/${params.id}`,
+          })
+        )
+
+        expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({
+          path: '/foo/1',
+          params: { id: 1 },
+          query: { a: 'a', b: 'b' },
+          hash: '#h',
+        })
+        expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({
+          path: '/foo/54',
+          params: { id: 54 },
+          query: { a: 'a', b: 'b' },
+          hash: '#h',
+        })
+      })
+
+      it('resolve string locations with query', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, {
+            match: query => ({
+              id: Array.isArray(query.id) ? query.id[0] : query.id,
+            }),
+            format: (params: { id: string }) => ({ id: Number(params.id) }),
+            unformat: (params: { id: number }) => ({ id: String(params.id) }),
+          })
+        )
+
+        expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({
+          params: { id: 100 },
+          path: '/foo',
+          query: {
+            id: '100',
+            b: 'b',
           },
-          format: (params: { id: string }) => ({ id: Number(params.id) }),
+          hash: '#h',
+        })
+      })
+
+      it('resolves string locations with hash', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern(
+            Symbol('foo'),
+            EMPTY_PATH_PATTERN_MATCHER,
+            undefined,
+            {
+              match: hash => hash,
+              format: hash => ({ a: hash.slice(1) }),
+              unformat: ({ a }) => '#a',
+            }
+          )
+        )
+
+        expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({
+          hash: '#bar',
+          params: { a: 'bar' },
+          path: '/foo',
+          query: { a: 'a', b: 'b' },
         })
-      )
+      })
 
-      expect(matcher.resolve('/foo/1?a=a&b=b#h')).toMatchObject({
-        path: '/foo/1',
-        params: { id: 1 },
-        query: { a: 'a', b: 'b' },
-        hash: '#h',
+      it('returns a valid location with an empty `matched` array if no match', () => {
+        const matcher = createCompiledMatcher()
+        expect(matcher.resolve('/bar')).toMatchInlineSnapshot(
+          {
+            hash: '',
+            matched: [],
+            params: {},
+            path: '/bar',
+            query: {},
+          },
+          `
+          {
+            "fullPath": "/bar",
+            "hash": "",
+            "matched": [],
+            "name": Symbol(no-match),
+            "params": {},
+            "path": "/bar",
+            "query": {},
+          }
+        `
+        )
       })
-      expect(matcher.resolve('/foo/54?a=a&b=b#h')).toMatchObject({
-        path: '/foo/54',
-        params: { id: 54 },
-        query: { a: 'a', b: 'b' },
-        hash: '#h',
+
+      it('resolves string locations with all', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern(
+            Symbol('foo'),
+            {
+              build: params => `/foo/${params.id}`,
+              match: path => {
+                const match = path.match(/^\/foo\/([^/]+?)$/)
+                if (!match) throw new Error('no match')
+                return { id: match[1] }
+              },
+              format: params => ({ id: Number(params.id) }),
+              unformat: params => ({ id: String(params.id) }),
+            },
+            {
+              match: query => ({
+                id: Array.isArray(query.id) ? query.id[0] : query.id,
+              }),
+              format: params => ({ q: Number(params.id) }),
+              unformat: params => ({ id: String(params.q) }),
+            },
+            {
+              match: hash => hash,
+              format: hash => ({ a: hash.slice(1) }),
+              unformat: ({ a }) => '#a',
+            }
+          )
+        )
+
+        expect(matcher.resolve('/foo/1?id=100#bar')).toMatchObject({
+          hash: '#bar',
+          params: { id: 1, q: 100, a: 'bar' },
+        })
       })
     })
 
-    it('resolve string locations with query', () => {
-      const matcher = createCompiledMatcher()
-      matcher.addRoute(
-        createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER, {
-          match: query => ({
-            id: Array.isArray(query.id) ? query.id[0] : query.id,
-          }),
-          format: (params: { id: string }) => ({ id: Number(params.id) }),
+    describe('relative locations as strings', () => {
+      it('resolves a simple relative location', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern(Symbol('foo'), EMPTY_PATH_PATTERN_MATCHER)
+        )
+
+        expect(
+          matcher.resolve('foo', matcher.resolve('/nested/'))
+        ).toMatchObject({
+          params: {},
+          path: '/nested/foo',
+          query: {},
+          hash: '',
+        })
+        expect(
+          matcher.resolve('../foo', matcher.resolve('/nested/'))
+        ).toMatchObject({
+          params: {},
+          path: '/foo',
+          query: {},
+          hash: '',
+        })
+        expect(
+          matcher.resolve('./foo', matcher.resolve('/nested/'))
+        ).toMatchObject({
+          params: {},
+          path: '/nested/foo',
+          query: {},
+          hash: '',
         })
-      )
-
-      expect(matcher.resolve('/foo?id=100&b=b#h')).toMatchObject({
-        params: { id: 100 },
-        path: '/foo',
-        query: {
-          id: '100',
-          b: 'b',
-        },
-        hash: '#h',
       })
     })
 
-    it('resolves string locations with hash', () => {
-      const matcher = createCompiledMatcher()
-      matcher.addRoute(
-        createMatcherPattern(
-          Symbol('foo'),
-          EMPTY_PATH_PATTERN_MATCHER,
-          undefined,
-          {
-            match: hash => hash,
-            format: hash => ({ a: hash.slice(1) }),
-          }
+    describe('named locations', () => {
+      it('resolves named locations with no params', () => {
+        const matcher = createCompiledMatcher()
+        matcher.addRoute(
+          createMatcherPattern('home', EMPTY_PATH_PATTERN_MATCHER)
         )
-      )
 
-      expect(matcher.resolve('/foo?a=a&b=b#bar')).toMatchObject({
-        hash: '#bar',
-        params: { a: 'bar' },
-        path: '/foo',
-        query: { a: 'a', b: 'b' },
+        expect(matcher.resolve({ name: 'home', params: {} })).toMatchObject({
+          name: 'home',
+          path: '/',
+          params: {},
+          query: {},
+          hash: '',
+        })
       })
     })
   })
index bd48a1246c6b931428c6e7917fd7310cbeaed415..5d204f7bcec78f79f847bf651a3597a492d6ed47 100644 (file)
@@ -187,6 +187,12 @@ function transformObject<T>(
   return encoded
 }
 
+export const NO_MATCH_LOCATION = {
+  name: Symbol('no-match'),
+  params: {},
+  matched: [],
+} satisfies Omit<NEW_MatcherLocationResolved, 'path' | 'hash' | 'query'>
+
 export function createCompiledMatcher(): NEW_Matcher_Resolve {
   const matchers = new Map<MatcherName, MatcherPattern>()
 
@@ -220,13 +226,21 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve {
           if (parsedParams) break
         }
       }
+
+      // No match location
       if (!parsedParams || !matcher) {
-        throw new Error(`No matcher found for location "${location}"`)
+        return {
+          ...url,
+          ...NO_MATCH_LOCATION,
+          query: transformObject(decode, decode, url.query),
+          hash: decode(url.hash),
+        }
       }
+
       // TODO: build fullPath
       return {
+        ...url,
         name: matcher.name,
-        path: url.path,
         params: parsedParams,
         query: transformObject(decode, decode, url.query),
         hash: decode(url.hash),
@@ -244,11 +258,6 @@ export function createCompiledMatcher(): NEW_Matcher_Resolve {
       const params = location.params ?? currentLocation!.params
       const mixedUnencodedParams = matcher.unformatParams(params)
 
-      // TODO: they could just throw?
-      if (!mixedUnencodedParams) {
-        throw new Error(`Missing params for matcher "${String(name)}"`)
-      }
-
       const path = matcher.buildPath(
         // encode the values before building the path
         transformObject(String, encodeParam, mixedUnencodedParams[0])