From: Eduardo San Martin Morote Date: Tue, 22 Jul 2025 13:31:19 +0000 (+0200) Subject: refactor: extended type of record X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=37aaddc0072197e0b92d4ff2125b10ad592c4d85;p=thirdparty%2Fvuejs%2Frouter.git refactor: extended type of record --- diff --git a/packages/router/src/experimental/router.ts b/packages/router/src/experimental/router.ts index 8083492e..60a70d58 100644 --- a/packages/router/src/experimental/router.ts +++ b/packages/router/src/experimental/router.ts @@ -178,32 +178,32 @@ export interface EXPERIMENTAL_RouterOptions_Base extends PathParserOptions { } // TODO: is it worth to have 2 types for the undefined values? -export interface EXPERIMENTAL_RouteRecordNormalized - extends EXPERIMENTAL_ResolverStaticRecord { - /** - * Arbitrary data attached to the record. - */ - meta: RouteMeta - - // TODO: - redirect?: unknown - - /** - * Allow passing down params as props to the component rendered by `router-view`. - */ - props: Record - - /** - * {@inheritDoc RouteRecordMultipleViews.components} - */ - components: Record - - /** - * Contains the original modules for lazy loaded components. - * @internal - */ - mods: Record -} +export type EXPERIMENTAL_RouteRecordNormalized = + EXPERIMENTAL_ResolverStaticRecord<{ + /** + * Arbitrary data attached to the record. + */ + meta: RouteMeta + + // TODO: + redirect?: unknown + + /** + * Allow passing down params as props to the component rendered by `router-view`. + */ + props: Record + + /** + * {@inheritDoc RouteRecordMultipleViews.components} + */ + components: Record + + /** + * Contains the original modules for lazy loaded components. + * @internal + */ + mods: Record + }> // TODO: probably need some generic types // , diff --git a/packages/router/src/new-route-resolver/matcher-pattern.ts b/packages/router/src/new-route-resolver/matcher-pattern.ts index e6eec914..35117c8a 100644 --- a/packages/router/src/new-route-resolver/matcher-pattern.ts +++ b/packages/router/src/new-route-resolver/matcher-pattern.ts @@ -1,7 +1,6 @@ import { decode, MatcherQueryParams } from './resolver' import { EmptyParams, MatcherParamsFormatted } from './matcher-location' import { miss } from './matchers/errors' -import { joinPaths } from './matcher-resolve.spec' /** * Base interface for matcher patterns that extract params from a URL. diff --git a/packages/router/src/new-route-resolver/resolver-static.spec.ts b/packages/router/src/new-route-resolver/resolver-static.spec.ts new file mode 100644 index 00000000..6f46abb3 --- /dev/null +++ b/packages/router/src/new-route-resolver/resolver-static.spec.ts @@ -0,0 +1,363 @@ +import { describe, expect, it } from 'vitest' +import { createStaticResolver } from './resolver-static' +import { MatcherQueryParams, NO_MATCH_LOCATION } from './resolver' +import { + MatcherPatternQuery, + MatcherPatternPathStatic, +} from './matcher-pattern' +import { + EMPTY_PATH_PATTERN_MATCHER, + USER_ID_PATH_PATTERN_MATCHER, + ANY_PATH_PATTERN_MATCHER, + ANY_HASH_PATTERN_MATCHER, +} from './matchers/test-utils' + +const PAGE_QUERY_PATTERN_MATCHER_LOCAL: MatcherPatternQuery<{ page: number }> = + { + match: query => { + const page = Number(query.page) + return { + page: Number.isNaN(page) ? 1 : page, + } + }, + build: params => ({ page: String(params.page) }), + } satisfies MatcherPatternQuery<{ page: number }> + +describe('StaticResolver', () => { + describe('new matchers', () => { + it('static path', () => { + const resolver = createStaticResolver([ + { name: 'root', path: new MatcherPatternPathStatic('/') }, + { name: 'users', path: new MatcherPatternPathStatic('/users') }, + ]) + + expect(resolver.resolve({ path: '/' })).toMatchObject({ + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + }) + + expect(resolver.resolve({ path: '/users' })).toMatchObject({ + fullPath: '/users', + path: '/users', + params: {}, + query: {}, + hash: '', + }) + }) + + it('dynamic path', () => { + const resolver = createStaticResolver([ + { + name: 'user-detail', + path: USER_ID_PATH_PATTERN_MATCHER, + }, + ]) + + expect(resolver.resolve({ path: '/users/1' })).toMatchObject({ + fullPath: '/users/1', + path: '/users/1', + params: { id: 1 }, + }) + }) + }) + + describe('resolve()', () => { + describe.todo('absolute locations as strings', () => { + it('resolves string locations with no params', () => { + const resolver = createStaticResolver([ + { name: 'root', path: EMPTY_PATH_PATTERN_MATCHER }, + ]) + + expect(resolver.resolve({ path: '/?a=a&b=b#h' })).toMatchObject({ + path: '/', + params: {}, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) + }) + + it('resolves a not found string', () => { + const resolver = createStaticResolver([]) + expect(resolver.resolve({ path: '/bar?q=1#hash' })).toEqual({ + ...NO_MATCH_LOCATION, + fullPath: '/bar?q=1#hash', + path: '/bar', + query: { q: '1' }, + hash: '#hash', + matched: [], + }) + }) + + it('resolves string locations with params', () => { + const resolver = createStaticResolver([ + { name: 'user-detail', path: USER_ID_PATH_PATTERN_MATCHER }, + ]) + + expect(resolver.resolve({ path: '/users/1?a=a&b=b#h' })).toMatchObject({ + path: '/users/1', + params: { id: 1 }, + query: { a: 'a', b: 'b' }, + hash: '#h', + }) + expect(resolver.resolve({ path: '/users/54?a=a&b=b#h' })).toMatchObject( + { + path: '/users/54', + params: { id: 54 }, + query: { a: 'a', b: 'b' }, + hash: '#h', + } + ) + }) + + it('resolve string locations with query', () => { + const resolver = createStaticResolver([ + { + name: 'any-path', + path: ANY_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER_LOCAL, + }, + ]) + + expect(resolver.resolve({ path: '/foo?page=100&b=b#h' })).toMatchObject( + { + params: { page: 100 }, + path: '/foo', + query: { + page: '100', + b: 'b', + }, + hash: '#h', + } + ) + }) + + it('resolves string locations with hash', () => { + const resolver = createStaticResolver([ + { + name: 'any-path', + path: ANY_PATH_PATTERN_MATCHER, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + expect(resolver.resolve({ path: '/foo?a=a&b=b#bar' })).toMatchObject({ + hash: '#bar', + params: { hash: 'bar' }, + path: '/foo', + query: { a: 'a', b: 'b' }, + }) + }) + + it('combines path, query and hash params', () => { + const resolver = createStaticResolver([ + { + name: 'user-detail', + path: USER_ID_PATH_PATTERN_MATCHER, + query: PAGE_QUERY_PATTERN_MATCHER_LOCAL, + hash: ANY_HASH_PATTERN_MATCHER, + }, + ]) + + expect( + resolver.resolve({ path: '/users/24?page=100#bar' }) + ).toMatchObject({ + params: { id: 24, page: 100, hash: 'bar' }, + }) + }) + }) + + describe('relative locations as strings', () => { + it('resolves a simple object relative location', () => { + const resolver = createStaticResolver([ + { name: 'any-path', path: ANY_PATH_PATTERN_MATCHER }, + ]) + + expect( + resolver.resolve( + { path: 'foo' }, + resolver.resolve({ path: '/nested/' }) + ) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', + }) + expect( + resolver.resolve( + { path: '../foo' }, + resolver.resolve({ path: '/nested/' }) + ) + ).toMatchObject({ + params: {}, + path: '/foo', + query: {}, + hash: '', + }) + expect( + resolver.resolve( + { path: './foo' }, + resolver.resolve({ path: '/nested/' }) + ) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', + }) + }) + }) + + it('resolves a simple string relative location', () => { + const resolver = createStaticResolver([ + { name: 'any-path', path: ANY_PATH_PATTERN_MATCHER }, + ]) + + expect( + resolver.resolve('foo', resolver.resolve({ path: '/nested/' })) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', + }) + expect( + resolver.resolve('../foo', resolver.resolve({ path: '/nested/' })) + ).toMatchObject({ + params: {}, + path: '/foo', + query: {}, + hash: '', + }) + expect( + resolver.resolve('./foo', resolver.resolve({ path: '/nested/' })) + ).toMatchObject({ + params: {}, + path: '/nested/foo', + query: {}, + hash: '', + }) + }) + + describe('absolute locations', () => { + it('resolves an object location', () => { + const resolver = createStaticResolver([ + { name: 'root', path: EMPTY_PATH_PATTERN_MATCHER }, + ]) + expect(resolver.resolve({ path: '/' })).toMatchObject({ + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + }) + }) + + it('resolves an absolute string location', () => { + const resolver = createStaticResolver([ + { name: 'root', path: EMPTY_PATH_PATTERN_MATCHER }, + ]) + expect(resolver.resolve('/')).toMatchObject({ + fullPath: '/', + path: '/', + params: {}, + query: {}, + hash: '', + }) + }) + }) + + describe('named locations', () => { + it('resolves named locations with no params', () => { + const resolver = createStaticResolver([ + { + name: 'home', + path: EMPTY_PATH_PATTERN_MATCHER, + }, + ]) + + expect(resolver.resolve({ name: 'home', params: {} })).toMatchObject({ + name: 'home', + path: '/', + params: {}, + query: {}, + hash: '', + }) + }) + }) + + describe('encoding', () => { + const resolver = createStaticResolver([ + { name: 'any-path', path: ANY_PATH_PATTERN_MATCHER }, + ]) + describe('decodes', () => { + it('handles encoded string path', () => { + expect(resolver.resolve({ path: '/%23%2F%3F' })).toMatchObject({ + fullPath: '/%23%2F%3F', + path: '/%23%2F%3F', + query: {}, + // we don't tests params here becuase it's matcher's responsibility to encode the path + hash: '', + }) + }) + + it('decodes query from a string', () => { + expect(resolver.resolve('/foo?foo=%23%2F%3F')).toMatchObject({ + path: '/foo', + fullPath: '/foo?foo=%23%2F%3F', + query: { foo: '#/?' }, + }) + }) + + it('passes a decoded query to the matcher', () => { + const resolver = createStaticResolver([ + { + name: 'query', + path: EMPTY_PATH_PATTERN_MATCHER, + query: { + match(q) { + return { q } + }, + build({ q }) { + return { ...q } + }, + } satisfies MatcherPatternQuery<{ q: MatcherQueryParams }>, + }, + ]) + expect(resolver.resolve('/?%23%2F%3F=%23%2F%3F')).toMatchObject({ + params: { q: { '#/?': '#/?' } }, + }) + }) + + it('decodes hash from a string', () => { + expect(resolver.resolve('/foo#%22')).toMatchObject({ + path: '/foo', + fullPath: '/foo#%22', + hash: '#"', + }) + }) + }) + + describe('encodes', () => { + it('encodes the query', () => { + expect( + resolver.resolve({ path: '/foo', query: { foo: '"' } }) + ).toMatchObject({ + fullPath: '/foo?foo=%22', + query: { foo: '"' }, + }) + }) + + it('encodes the hash', () => { + expect(resolver.resolve({ path: '/foo', hash: '#"' })).toMatchObject({ + fullPath: '/foo#%22', + hash: '#"', + }) + }) + }) + }) + }) +}) diff --git a/packages/router/src/new-route-resolver/resolver-static.ts b/packages/router/src/new-route-resolver/resolver-static.ts index 669d5589..8c67f669 100644 --- a/packages/router/src/new-route-resolver/resolver-static.ts +++ b/packages/router/src/new-route-resolver/resolver-static.ts @@ -75,11 +75,12 @@ export interface EXPERIMENTAL_ResolverRecord_Matchable path: MatcherPatternPath } -export type EXPERIMENTAL_ResolverRecord = - | EXPERIMENTAL_ResolverRecord_Matchable - | EXPERIMENTAL_ResolverRecord_Group +export type EXPERIMENTAL_ResolverRecord = + | (EXPERIMENTAL_ResolverRecord_Matchable & T) + | (EXPERIMENTAL_ResolverRecord_Group & T) -export type EXPERIMENTAL_ResolverStaticRecord = EXPERIMENTAL_ResolverRecord +export type EXPERIMENTAL_ResolverStaticRecord = + EXPERIMENTAL_ResolverRecord export interface EXPERIMENTAL_ResolverStatic extends NEW_RouterResolver_Base {}