]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: resolve relative paths
authorEduardo San Martin Morote <posva13@gmail.com>
Wed, 29 Apr 2020 11:41:53 +0000 (13:41 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Wed, 29 Apr 2020 11:41:53 +0000 (13:41 +0200)
__tests__/location.spec.ts
__tests__/matcher/resolve.spec.ts
src/location.ts
src/matcher/index.ts
src/router.ts

index 252f39bb874191a527dee543df1e2d54b6e9d420..e03849506834699c7693e1806d05840c7bd11553 100644 (file)
@@ -19,6 +19,78 @@ describe('parseURL', () => {
     })
   })
 
+  it('works with partial path with no query', () => {
+    expect(parseURL('foo#hash')).toEqual({
+      fullPath: '/foo#hash',
+      path: '/foo',
+      hash: '#hash',
+      query: {},
+    })
+  })
+
+  it('works with partial path', () => {
+    expect(parseURL('foo?f=foo#hash')).toEqual({
+      fullPath: '/foo?f=foo#hash',
+      path: '/foo',
+      hash: '#hash',
+      query: { f: 'foo' },
+    })
+  })
+
+  it('works with only query', () => {
+    expect(parseURL('?f=foo')).toEqual({
+      fullPath: '/?f=foo',
+      path: '/',
+      hash: '',
+      query: { f: 'foo' },
+    })
+  })
+
+  it('works with only hash', () => {
+    expect(parseURL('#foo')).toEqual({
+      fullPath: '/#foo',
+      path: '/',
+      hash: '#foo',
+      query: {},
+    })
+  })
+
+  it('works with partial path and current location', () => {
+    expect(parseURL('foo', '/parent/bar')).toEqual({
+      fullPath: '/parent/foo',
+      path: '/parent/foo',
+      hash: '',
+      query: {},
+    })
+  })
+
+  it('works with partial path with query and hash and current location', () => {
+    expect(parseURL('foo?f=foo#hash', '/parent/bar')).toEqual({
+      fullPath: '/parent/foo?f=foo#hash',
+      path: '/parent/foo',
+      hash: '#hash',
+      query: { f: 'foo' },
+    })
+  })
+
+  it('works with relative query and current location', () => {
+    expect(parseURL('?f=foo', '/parent/bar')).toEqual({
+      fullPath: '/parent/bar?f=foo',
+      path: '/parent/bar',
+      hash: '',
+      query: { f: 'foo' },
+    })
+  })
+
+  it('works with relative hash and current location', () => {
+    expect(parseURL('#hash', '/parent/bar')).toEqual({
+      fullPath: '/parent/bar#hash',
+      path: '/parent/bar',
+      hash: '#hash',
+      query: {},
+    })
+  })
+
   it('extracts the query', () => {
     expect(parseURL('/foo?a=one&b=two')).toEqual({
       fullPath: '/foo?a=one&b=two',
index b292abf8a4a2be927eeeba2332bb87ae3568b399..5b9495cc733a3479e079c1d914628ea0646a4042 100644 (file)
@@ -7,6 +7,7 @@ import {
   MatcherLocation,
 } from '../../src/types'
 import { MatcherLocationNormalizedLoose } from '../utils'
+import { mockWarn } from 'jest-mock-warn'
 
 // @ts-ignore
 const component: RouteComponent = null
@@ -756,6 +757,26 @@ describe('RouterMatcher.resolve', () => {
   })
 
   describe('LocationAsRelative', () => {
+    mockWarn()
+    it('warns if a path isn not absolute', () => {
+      const record = {
+        path: '/parent',
+        components,
+      }
+      const matcher = createRouterMatcher([record], {})
+      matcher.resolve(
+        { path: 'two' },
+        {
+          path: '/parent/one',
+          name: undefined,
+          params: {},
+          matched: [] as any,
+          meta: {},
+        }
+      )
+      expect('received "two"').toHaveBeenWarned()
+    })
+
     it('matches with nothing', () => {
       const record = { path: '/home', name: 'Home', components }
       assertRecordMatch(
index 677252da6f8310ad19d5186d523703ca7137cf23..9b56130baa789c410b96cc68746fe6a7573347b7 100644 (file)
@@ -36,13 +36,16 @@ export const removeTrailingSlash = (path: string) =>
  *
  * @param parseQuery
  * @param location - URI to normalize
+ * @param currentLocation - current absolute location. Allows resolving relative
+ * paths. Must start with `/`. Defaults to `/`
  * @returns a normalized history location
  */
 export function parseURL(
   parseQuery: (search: string) => LocationQuery,
-  location: string
+  location: string,
+  currentLocation: string = '/'
 ): LocationNormalized {
-  let path = '',
+  let path: string | undefined,
     query: LocationQuery = {},
     searchString = '',
     hash = ''
@@ -68,10 +71,19 @@ export function parseURL(
   }
 
   // no search and no query
-  path = path || location
+  path = path != null ? path : location
+  // empty path means a relative query or hash `?foo=f`, `#thing`
+  if (!path) {
+    path = currentLocation + path
+  } else if (path[0] !== '/') {
+    // relative to current location. Currently we only support simple relative
+    // but no `..`, `.`, or complex like `../.././..`. We will always leave the
+    // leading slash so we can safely append path
+    path = currentLocation.replace(/[^\/]*$/, '') + path
+  }
 
   return {
-    fullPath: location,
+    fullPath: path + (searchString && '?') + searchString + hash,
     path,
     query,
     hash,
index 9cd960dfdfc87a109bac79af628a3d5a5928f59a..8c4b4a2f374ca57ff365b8b005faf8933351b2cf 100644 (file)
@@ -13,6 +13,7 @@ import {
   comparePathParserScore,
   PathParserOptions,
 } from './pathParserRanker'
+import { warn } from 'vue'
 
 let noop = () => {}
 
@@ -208,15 +209,22 @@ export function createRouterMatcher(
       // throws if cannot be stringified
       path = matcher.stringify(params)
     } else if ('path' in location) {
-      matcher = matchers.find(m => m.re.test(location.path))
-      // matcher should have a value after the loop
-
       // no need to resolve the path with the matcher as it was provided
       // this also allows the user to control the encoding
       path = location.path
+
+      if (__DEV__ && path[0] !== '/') {
+        warn(
+          `The Matcher cannot resolve relative paths but received "${path}". Unless you directly called \`matcher.resolve("${path}")\`, this is probably a bug in vue-router. Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/vue-router-next.`
+        )
+      }
+
+      matcher = matchers.find(m => m.re.test(path))
+      // matcher should have a value after the loop
+
       if (matcher) {
         // TODO: dev warning of unused params if provided
-        params = matcher.parse(location.path)!
+        params = matcher.parse(path)!
         name = matcher.record.name
       }
       // location is a relative path
index 1d0702dd6533626ffc87c9f105596a4e2b927f31..dde44d7673c03a17bf67f1cf8cb0a5f998d0d1c0 100644 (file)
@@ -207,13 +207,17 @@ export function createRouter({
   }
 
   function resolve(
-    location: RouteLocationRaw,
-    currentLocation?: RouteLocationNormalizedLoaded
+    location: Readonly<RouteLocationRaw>,
+    currentLocation?: Readonly<RouteLocationNormalizedLoaded>
   ): RouteLocation & { href: string } {
     // const objectLocation = routerLocationAsObject(location)
     currentLocation = currentLocation || currentRoute.value
     if (typeof location === 'string') {
-      let locationNormalized = parseURL(parseQuery, location)
+      let locationNormalized = parseURL(
+        parseQuery,
+        location,
+        currentLocation.path
+      )
       let matchedRoute = matcher.resolve(
         { path: locationNormalized.path },
         currentLocation
@@ -235,6 +239,16 @@ export function createRouter({
       }
     }
 
+    // TODO: dev warning if params and path at the same time
+
+    // path could be relative in object as well
+    if ('path' in location) {
+      location = {
+        ...location,
+        path: parseURL(parseQuery, location.path, currentLocation.path).path,
+      }
+    }
+
     let matchedRoute: MatcherLocation = // relative or named location, path is ignored
       // for same reason TS thinks location.params can be undefined
       matcher.resolve(
@@ -473,7 +487,7 @@ export function createRouter({
         return runGuardQueue(guards)
       })
       .then(() => {
-        // check global guards beforeEach
+        // check global guards beforeResolve
         guards = []
         for (const guard of beforeResolveGuards.list()) {
           guards.push(guardToPromiseFn(guard, to, from))