From f20dbf1c634c5a9e0d8c4c88ce3112b514f622fd Mon Sep 17 00:00:00 2001 From: Eduardo San Martin Morote Date: Thu, 9 Jan 2025 11:54:20 +0100 Subject: [PATCH] feat: allow string in matcher resolve --- packages/router/src/experimental/router.ts | 23 +++--- packages/router/src/location.ts | 2 +- .../src/new-route-resolver/resolver.spec.ts | 7 +- .../src/new-route-resolver/resolver.test-d.ts | 22 +++++- .../router/src/new-route-resolver/resolver.ts | 77 ++++++++++++------- 5 files changed, 78 insertions(+), 53 deletions(-) diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 1b252336e..a6c70afff 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -83,7 +83,6 @@ import { routerKey, routerViewLocationKey, } from '../injectionSymbols' -import { MatcherLocationAsPathAbsolute } from '../new-route-resolver/matcher-location' /** * resolve, reject arguments of Promise constructor @@ -537,11 +536,6 @@ 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( @@ -551,9 +545,12 @@ export function experimental_createRouter( return resolve({}) } - if (!locationObject.hash?.startsWith('#')) { + if ( + typeof rawLocation === 'object' && + rawLocation.hash?.startsWith('#') + ) { warn( - `A \`hash\` should always start with the character "#". Replace "${locationObject.hash}" with "#${locationObject.hash}".` + `A \`hash\` should always start with the character "#". Replace "${rawLocation.hash}" with "#${rawLocation.hash}".` ) } } @@ -571,12 +568,10 @@ export function experimental_createRouter( // } const matchedRoute = matcher.resolve( - // FIXME: should be ok - // 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 + // incompatible types + rawLocation as any, + // incompatible `matched` requires casting + currentLocation as any ) const href = routerHistory.createHref(matchedRoute.fullPath) diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index 8ab2a1815..57d4e589d 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -10,7 +10,7 @@ import { RouteLocation, RouteLocationNormalizedLoaded } from './typed-routes' * Location object returned by {@link `parseURL`}. * @internal */ -interface LocationNormalized { +export interface LocationNormalized { path: string fullPath: string hash: string diff --git a/packages/router/src/new-route-resolver/resolver.spec.ts b/packages/router/src/new-route-resolver/resolver.spec.ts index ecc2d5e39..436349a04 100644 --- a/packages/router/src/new-route-resolver/resolver.spec.ts +++ b/packages/router/src/new-route-resolver/resolver.spec.ts @@ -337,9 +337,7 @@ describe('RouterMatcher', () => { }) }) - // 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 + it('decodes query from a string', () => { expect(matcher.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ path: '/foo', fullPath: '/foo?foo=%23%2F%3F', @@ -347,8 +345,7 @@ describe('RouterMatcher', () => { }) }) - it.todo('decodes hash from a string', () => { - // @ts-expect-error: does not suppor fullPath + it('decodes hash from a string', () => { expect(matcher.resolve('/foo#%22')).toMatchObject({ path: '/foo', fullPath: '/foo#%22', diff --git a/packages/router/src/new-route-resolver/resolver.test-d.ts b/packages/router/src/new-route-resolver/resolver.test-d.ts index c04dfad31..6da64da51 100644 --- a/packages/router/src/new-route-resolver/resolver.test-d.ts +++ b/packages/router/src/new-route-resolver/resolver.test-d.ts @@ -18,11 +18,16 @@ describe('Matcher', () => { expectTypeOf(matcher.resolve({ path: '/foo' })).toEqualTypeOf< NEW_LocationResolved >() + expectTypeOf(matcher.resolve('/foo')).toEqualTypeOf< + NEW_LocationResolved + >() }) it('fails on non absolute location without a currentLocation', () => { // @ts-expect-error: needs currentLocation matcher.resolve('foo') + // @ts-expect-error: needs currentLocation + matcher.resolve({ path: 'foo' }) }) it('resolves relative locations', () => { @@ -32,6 +37,9 @@ describe('Matcher', () => { {} as NEW_LocationResolved ) ).toEqualTypeOf>() + expectTypeOf( + matcher.resolve('foo', {} as NEW_LocationResolved) + ).toEqualTypeOf>() }) it('resolved named locations', () => { @@ -42,7 +50,9 @@ describe('Matcher', () => { it('fails on object relative location without a currentLocation', () => { // @ts-expect-error: needs currentLocation - matcher.resolve({ params: { id: 1 } }) + matcher.resolve({ params: { id: '1' } }) + // @ts-expect-error: needs currentLocation + matcher.resolve({ query: { id: '1' } }) }) it('resolves object relative locations with a currentLocation', () => { @@ -57,13 +67,17 @@ describe('Matcher', () => { it('does not allow a name + path', () => { matcher.resolve({ - // ...({} as NEW_LocationResolved), + // ...({} as NEW_LocationResolved), name: 'foo', params: {}, // @ts-expect-error: name + path path: '/e', }) - // @ts-expect-error: name + currentLocation - matcher.resolve({ name: 'a', params: {} }, {} as NEW_LocationResolved) + matcher.resolve( + // @ts-expect-error: name + currentLocation + { name: 'a', params: {} }, + // + {} as NEW_LocationResolved + ) }) }) diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index 93b235c79..060aee34c 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -1,4 +1,9 @@ -import { type LocationQuery, normalizeQuery, stringifyQuery } from '../query' +import { + type LocationQuery, + normalizeQuery, + parseQuery, + stringifyQuery, +} from '../query' import type { MatcherPatternHash, MatcherPatternPath, @@ -6,7 +11,12 @@ import type { } from './matcher-pattern' import { warn } from '../warning' import { encodeQueryValue as _encodeQueryValue, encodeParam } from '../encoding' -import { NEW_stringifyURL, resolveRelativePath } from '../location' +import { + LocationNormalized, + NEW_stringifyURL, + parseURL, + resolveRelativePath, +} from '../location' import type { MatcherLocationAsNamed, MatcherLocationAsPathAbsolute, @@ -32,19 +42,19 @@ export interface NEW_RouterResolver { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - // resolve( - // absoluteLocation: `/${string}`, - // currentLocation?: undefined | NEW_LocationResolved - // ): NEW_LocationResolved + resolve( + absoluteLocation: `/${string}`, + currentLocation?: undefined + ): NEW_LocationResolved /** * 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 - // ): NEW_LocationResolved + resolve( + relativeLocation: string, + currentLocation: NEW_LocationResolved + ): NEW_LocationResolved /** * Resolves a location by its name. Any required params or query must be passed in the `options` argument. @@ -53,6 +63,7 @@ export interface NEW_RouterResolver { location: MatcherLocationAsNamed, // TODO: is this useful? currentLocation?: undefined + // currentLocation?: undefined | NEW_LocationResolved ): NEW_LocationResolved /** @@ -63,7 +74,7 @@ export interface NEW_RouterResolver { location: MatcherLocationAsPathAbsolute, // TODO: is this useful? currentLocation?: undefined - // currentLocation?: NEW_LocationResolved + // currentLocation?: NEW_LocationResolved | undefined ): NEW_LocationResolved resolve( @@ -121,7 +132,7 @@ export interface NEW_RouterResolver { */ export type MatcherLocationRaw = // | `/${string}` - // | string + | string | MatcherLocationAsNamed | MatcherLocationAsPathAbsolute | MatcherLocationAsPathRelative @@ -355,23 +366,27 @@ export function createCompiledMatcher< // NOTE: because of the overloads, we need to manually type the arguments type MatcherResolveArgs = - // | [ - // absoluteLocation: `/${string}`, - // currentLocation?: undefined | NEW_LocationResolved - // ] - // | [ - // relativeLocation: string, - // currentLocation: NEW_LocationResolved - // ] + | [absoluteLocation: `/${string}`, currentLocation?: undefined] + | [ + relativeLocation: string, + currentLocation: NEW_LocationResolved + ] | [ absoluteLocation: MatcherLocationAsPathAbsolute, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined currentLocation?: undefined ] | [ relativeLocation: MatcherLocationAsPathRelative, currentLocation: NEW_LocationResolved ] - | [location: MatcherLocationAsNamed, currentLocation?: undefined] + | [ + location: MatcherLocationAsNamed, + // Same as above + // currentLocation?: NEW_LocationResolved | undefined + currentLocation?: undefined + ] | [ relativeLocation: MatcherLocationAsRelative, currentLocation: NEW_LocationResolved @@ -382,7 +397,7 @@ export function createCompiledMatcher< ): NEW_LocationResolved { const [to, currentLocation] = args - if (to.name || to.path == null) { + if (typeof to === 'object' && (to.name || to.path == null)) { // relative location or by name if (__DEV__ && to.name == null && currentLocation == null) { console.warn( @@ -442,13 +457,17 @@ export function createCompiledMatcher< // string location, e.g. '/foo', '../bar', 'baz', '?page=1' } else { // parseURL handles relative paths - // 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 url: LocationNormalized + if (typeof to === 'string') { + url = parseURL(parseQuery, to, currentLocation?.path) + } else { + const query = normalizeQuery(to.query) + url = { + fullPath: NEW_stringifyURL(stringifyQuery, to.path, query, to.hash), + path: resolveRelativePath(to.path, currentLocation?.path || '/'), + query, + hash: to.hash || '', + } } let matcher: TMatcherRecord | undefined