From: Eduardo San Martin Morote Date: Wed, 29 Apr 2020 11:41:53 +0000 (+0200) Subject: feat: resolve relative paths X-Git-Tag: v4.0.0-alpha.8~12 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=eae833e0fc1c8e549f2b4cd47b3dcb90484d17d5;p=thirdparty%2Fvuejs%2Frouter.git feat: resolve relative paths --- diff --git a/__tests__/location.spec.ts b/__tests__/location.spec.ts index 252f39bb..e0384950 100644 --- a/__tests__/location.spec.ts +++ b/__tests__/location.spec.ts @@ -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', diff --git a/__tests__/matcher/resolve.spec.ts b/__tests__/matcher/resolve.spec.ts index b292abf8..5b9495cc 100644 --- a/__tests__/matcher/resolve.spec.ts +++ b/__tests__/matcher/resolve.spec.ts @@ -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( diff --git a/src/location.ts b/src/location.ts index 677252da..9b56130b 100644 --- a/src/location.ts +++ b/src/location.ts @@ -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, diff --git a/src/matcher/index.ts b/src/matcher/index.ts index 9cd960df..8c4b4a2f 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -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 diff --git a/src/router.ts b/src/router.ts index 1d0702dd..dde44d76 100644 --- a/src/router.ts +++ b/src/router.ts @@ -207,13 +207,17 @@ export function createRouter({ } function resolve( - location: RouteLocationRaw, - currentLocation?: RouteLocationNormalizedLoaded + location: Readonly, + currentLocation?: Readonly ): 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))