diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 32f8a0f0f..1b252336e 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -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 + 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 | 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 ) 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 | RouteLocationNormalized { - return typeof to === 'string' - ? parseURL(parseQuery, to, currentRoute.value.path) - : assign({}, to) - } - function checkCanceledNavigation( to: RouteLocationNormalized, from: RouteLocationNormalized diff --git a/packages/router/src/location.ts b/packages/router/src/location.ts index 163bf6f8d..8ab2a1815 100644 --- a/packages/router/src/location.ts +++ b/packages/router/src/location.ts @@ -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` diff --git a/packages/router/src/new-route-resolver/matcher-location.ts b/packages/router/src/new-route-resolver/matcher-location.ts index f597df07f..e05fdf7b3 100644 --- a/packages/router/src/new-route-resolver/matcher-location.ts +++ b/packages/router/src/new-route-resolver/matcher-location.ts @@ -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}` diff --git a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts index 6ad889394..a73a05841 100644 --- a/packages/router/src/new-route-resolver/matcher-resolve.spec.ts +++ b/packages/router/src/new-route-resolver/matcher-resolve.spec.ts @@ -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() @@ -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 | `/${string}`, - fromLocation: - | NEW_LocationResolved - // 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 ) }) diff --git a/packages/router/src/new-route-resolver/matcher.spec.ts b/packages/router/src/new-route-resolver/matcher.spec.ts index 335ddb83d..ecc2d5e39 100644 --- a/packages/router/src/new-route-resolver/matcher.spec.ts +++ b/packages/router/src/new-route-resolver/matcher.spec.ts @@ -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', diff --git a/packages/router/src/new-route-resolver/matcher.test-d.ts b/packages/router/src/new-route-resolver/matcher.test-d.ts index 26060c3a6..c04dfad31 100644 --- a/packages/router/src/new-route-resolver/matcher.test-d.ts +++ b/packages/router/src/new-route-resolver/matcher.test-d.ts @@ -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 >() }) @@ -27,7 +27,10 @@ describe('Matcher', () => { it('resolves relative locations', () => { expectTypeOf( - matcher.resolve('foo', {} as NEW_LocationResolved) + matcher.resolve( + { path: 'foo' }, + {} as NEW_LocationResolved + ) ).toEqualTypeOf>() }) diff --git a/packages/router/src/new-route-resolver/resolver.ts b/packages/router/src/new-route-resolver/resolver.ts index 17ceecf02..93b235c79 100644 --- a/packages/router/src/new-route-resolver/resolver.ts +++ b/packages/router/src/new-route-resolver/resolver.ts @@ -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 { /** * Resolves an absolute location (like `/path/to/somewhere`). */ - resolve( - absoluteLocation: `/${string}`, - currentLocation?: undefined | NEW_LocationResolved - ): NEW_LocationResolved + // resolve( + // absoluteLocation: `/${string}`, + // currentLocation?: undefined | NEW_LocationResolved + // ): 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. */ resolve( - location: MatcherLocationAsNamed + location: MatcherLocationAsNamed, + // TODO: is this useful? + currentLocation?: undefined ): NEW_LocationResolved /** @@ -63,7 +60,10 @@ export interface NEW_RouterResolver { * @param location - The location to resolve. */ resolve( - location: MatcherLocationAsPathAbsolute + location: MatcherLocationAsPathAbsolute, + // TODO: is this useful? + currentLocation?: undefined + // currentLocation?: NEW_LocationResolved ): NEW_LocationResolved resolve( @@ -120,8 +120,8 @@ export interface NEW_RouterResolver { * 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 { @@ -282,6 +287,8 @@ export interface NEW_MatcherRecordBase { 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 + // ] + // | [ + // relativeLocation: string, + // currentLocation: NEW_LocationResolved + // ] | [ - absoluteLocation: `/${string}`, - currentLocation?: undefined | NEW_LocationResolved - ] - | [ - relativeLocation: string, - currentLocation: NEW_LocationResolved + absoluteLocation: MatcherLocationAsPathAbsolute, + currentLocation?: undefined ] - | [absoluteLocation: MatcherLocationAsPathAbsolute] | [ relativeLocation: MatcherLocationAsPathRelative, currentLocation: NEW_LocationResolved ] - | [location: MatcherLocationAsNamed] + | [location: MatcherLocationAsNamed, currentLocation?: undefined] | [ relativeLocation: MatcherLocationAsRelative, currentLocation: NEW_LocationResolved @@ -370,12 +380,76 @@ export function createCompiledMatcher< function resolve( ...args: MatcherResolveArgs ): NEW_LocationResolved { - 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['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, } diff --git a/packages/router/src/query.ts b/packages/router/src/query.ts index 55e77c714..79feb6e43 100644 --- a/packages/router/src/query.ts +++ b/packages/router/src/query.ts @@ -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 */