From 98f08cb5ee85f195c4f7db5fd183df09323672be Mon Sep 17 00:00:00 2001 From: Bobbie Goede Date: Sat, 4 Nov 2023 03:22:52 +0100 Subject: [PATCH] refactor: routing compatibles (#79) --- .../src/__test__/compatibles.test.ts | 36 ++- .../src/__test__/utils.test.ts | 286 ------------------ .../src/compatibles/routing.ts | 102 ++++--- .../vue-i18n-routing/src/compatibles/utils.ts | 88 +++--- packages/vue-i18n-routing/src/utils.ts | 2 +- 5 files changed, 125 insertions(+), 389 deletions(-) diff --git a/packages/vue-i18n-routing/src/__test__/compatibles.test.ts b/packages/vue-i18n-routing/src/__test__/compatibles.test.ts index bb74eda..db2872b 100644 --- a/packages/vue-i18n-routing/src/__test__/compatibles.test.ts +++ b/packages/vue-i18n-routing/src/__test__/compatibles.test.ts @@ -76,6 +76,11 @@ describe('localePath', () => { routes: [ { path: '/', name: 'index', component: { template: '
index
' } }, { path: '/about', name: 'about', component: { template: '
About
' } }, + { + path: '/path/:param', + name: 'as-a-test', + component: { template: '
Testing
' } + }, { path: '/:pathMatch(.*)*', name: 'not-found', component: { template: '
Not Found
' } } ], history: createMemoryHistory() @@ -106,6 +111,12 @@ describe('localePath', () => { assert.equal(vm.localePath('/?foo=1'), '/ja?foo=1') assert.equal(vm.localePath('/about?foo=1'), '/ja/about?foo=1') assert.equal(vm.localePath('/about?foo=1&test=2'), '/ja/about?foo=1&test=2') + assert.equal(vm.localePath('/path/as a test?foo=bar sentence'), '/ja/path/as a test?foo=bar+sentence') + assert.equal( + vm.localePath('/path/as%20a%20test?foo=bar%20sentence'), + '/ja/path/as%20a%20test?foo=bar+sentence' + ) + assert.equal(vm.localePath({ path: '/about', hash: '#foo=bar' }), '/ja/about#foo=bar') // no define path assert.equal(vm.localePath('/vue-i18n'), '/ja/vue-i18n') @@ -397,6 +408,11 @@ describe('switchLocalePath', () => { name: 'count', component: { template: '
Category
' } }, + { + path: '/as a test', + name: 'as-a-test', + component: { template: '
Testing
' } + }, { path: '/:pathMatch(.*)*', name: 'not-found', @@ -443,20 +459,28 @@ describe('switchLocalePath', () => { assert.equal(vm.switchLocalePath('fr'), '/fr/about?foo=b%C3%A4r&four=%E5%9B%9B') assert.equal(vm.switchLocalePath('en'), '/en/about?foo=b%C3%A4r&four=%E5%9B%9B') + await router.push('/ja/about#foo=bar') + assert.equal(vm.switchLocalePath('ja'), '/ja/about#foo=bar') + assert.equal(vm.switchLocalePath('fr'), '/fr/about#foo=bar') + assert.equal(vm.switchLocalePath('en'), '/en/about#foo=bar') + + await router.push('/ja/about?foo=é') + assert.equal(vm.switchLocalePath('ja'), '/ja/about?foo=%C3%A9') + await router.push('/ja/category/1') assert.equal(vm.switchLocalePath('ja'), '/ja/category/japanese') assert.equal(vm.switchLocalePath('en'), '/en/category/english') assert.equal(vm.switchLocalePath('fr'), '/fr/category/franch') await router.push('/ja/count/三') - assert.equal(vm.switchLocalePath('ja'), '/ja/count/%E4%B8%89') - assert.equal(vm.switchLocalePath('en'), '/en/count/%E4%B8%89') - assert.equal(vm.switchLocalePath('fr'), '/fr/count/%E4%B8%89') + assert.equal(vm.switchLocalePath('ja'), '/ja/count/三') + assert.equal(vm.switchLocalePath('en'), '/en/count/三') + assert.equal(vm.switchLocalePath('fr'), '/fr/count/三') await router.push('/ja/count/三?foo=bär&four=四&foo=bar') - assert.equal(vm.switchLocalePath('ja'), '/ja/count/%E4%B8%89?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') - assert.equal(vm.switchLocalePath('fr'), '/fr/count/%E4%B8%89?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') - assert.equal(vm.switchLocalePath('en'), '/en/count/%E4%B8%89?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') + assert.equal(vm.switchLocalePath('ja'), '/ja/count/三?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') + assert.equal(vm.switchLocalePath('fr'), '/fr/count/三?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') + assert.equal(vm.switchLocalePath('en'), '/en/count/三?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B') await router.push('/ja/foo') assert.equal(vm.switchLocalePath('ja'), '/ja/not-found-japanese') diff --git a/packages/vue-i18n-routing/src/__test__/utils.test.ts b/packages/vue-i18n-routing/src/__test__/utils.test.ts index 5655319..de2310a 100644 --- a/packages/vue-i18n-routing/src/__test__/utils.test.ts +++ b/packages/vue-i18n-routing/src/__test__/utils.test.ts @@ -1,6 +1,5 @@ import { describe, it, assert, test } from 'vitest' -import { resolvedRouteToObject } from '../compatibles/utils' import { adjustRoutePathForTrailingSlash, getLocaleRouteName, findBrowserLocale } from '../utils' import type { BrowserLocale } from '../utils' @@ -221,288 +220,3 @@ describe('findBrowserLocale', () => { assert.ok(locale === 'ja') }) }) - -describe('resolvedRouteToObject', () => { - it('should map resolved route without special characters', () => { - const expected = { - fullPath: '/ja/about', - hash: '', - query: {}, - name: 'about___ja', - path: '/ja/about', - params: {}, - matched: [ - { - path: '/ja/about', - redirect: undefined, - name: 'about___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
About
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/about' - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepEqual(resolvedRouteToObject(expected as any), expected as any) - }) - - it('should map resolved route without special characters and query', () => { - const expected = { - fullPath: '/ja/about?foo=1&test=2', - hash: '', - query: { - foo: '1', - test: '2' - }, - name: 'about___ja', - path: '/ja/about', - params: {}, - matched: [ - { - path: '/ja/about', - redirect: undefined, - name: 'about___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
About
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/about?foo=1&test=2' - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepEqual(resolvedRouteToObject(expected as any), expected as any) - }) - - it('should map resolved route without special characters and query with special characters', () => { - const expected = { - fullPath: '/ja/about?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B', - hash: '', - query: { - foo: ['bär', 'bar'], - four: '四' - }, - name: 'about___ja', - path: '/ja/about', - params: {}, - matched: [ - { - path: '/ja/about', - redirect: undefined, - name: 'about___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
About
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/about?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B' - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepEqual(resolvedRouteToObject(expected as any), expected as any) - }) - - it('should map resolved route with special characters and query with special characters', () => { - const provided = { - fullPath: '/ja/count/三?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B', - hash: '', - query: { - foo: ['bär', 'bar'], - four: '四' - }, - name: 'count___ja', - path: '/ja/count/三', - params: { - id: '三' - }, - matched: [ - { - path: '/ja/count/:id', - redirect: undefined, - name: 'count___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
Category
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/count/%E5%9B%9B?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B' - } - const expected = { - fullPath: '/ja/count/%E4%B8%89?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B', - hash: '', - query: { - foo: ['bär', 'bar'], - four: '四' - }, - name: 'count___ja', - path: '/ja/count/%E4%B8%89', - params: { - id: '三' - }, - matched: [ - { - path: '/ja/count/:id', - redirect: undefined, - name: 'count___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
Category
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/count/%E4%B8%89?foo=b%C3%A4r&foo=bar&four=%E5%9B%9B' - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepEqual(resolvedRouteToObject(provided as any), expected as any) - }) - - it('should map resolved route with special characters', () => { - const provided = { - fullPath: '/ja/count/三', - hash: '', - query: {}, - name: 'count___ja', - path: '/ja/count/三', - params: { - id: '三' - }, - matched: [ - { - path: '/ja/count/:id', - redirect: undefined, - name: 'count___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
Category
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/count/三' - } - const expected = { - fullPath: '/ja/count/%E4%B8%89', - hash: '', - query: {}, - name: 'count___ja', - path: '/ja/count/%E4%B8%89', - params: { - id: '三' - }, - matched: [ - { - path: '/ja/count/:id', - redirect: undefined, - name: 'count___ja', - meta: {}, - aliasOf: undefined, - beforeEnter: undefined, - props: { - default: false - }, - children: [], - instances: {}, - leaveGuards: {}, - updateGuards: {}, - enterCallbacks: {}, - components: { - default: { - template: '
Category
' - } - } - } - ], - meta: {}, - redirectedFrom: undefined, - href: '/ja/count/%E4%B8%89' - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - assert.deepEqual(resolvedRouteToObject(provided as any), expected as any) - }) -}) diff --git a/packages/vue-i18n-routing/src/compatibles/routing.ts b/packages/vue-i18n-routing/src/compatibles/routing.ts index 3c32539..015dc78 100644 --- a/packages/vue-i18n-routing/src/compatibles/routing.ts +++ b/packages/vue-i18n-routing/src/compatibles/routing.ts @@ -1,24 +1,28 @@ import { isString, assign } from '@intlify/shared' -import { withTrailingSlash, withoutTrailingSlash } from 'ufo' +import { + type Route, + type RawLocation, + type RouteLocationRaw, + type RouteLocationNormalizedLoaded, + type Router, + type RouteMeta, + type RouteLocationNamedRaw, + type RouteLocationPathRaw, + type RouteLocation, + type Location +} from '@intlify/vue-router-bridge' +import { parsePath, parseQuery, withTrailingSlash, withoutTrailingSlash } from 'ufo' import { isVue3, isRef, unref, isVue2 } from 'vue-demi' import { DEFAULT_DYNAMIC_PARAMS_KEY } from '../constants' import { getLocale, getLocaleRouteName, getRouteName } from '../utils' -import { getI18nRoutingOptions, isV4Route, resolve, resolvedRouteToObject, routeToObject } from './utils' +import { getI18nRoutingOptions, isV4Route, resolve, routeToObject } from './utils' import type { RoutingProxy, PrefixableOptions, SwitchLocalePathIntercepter } from './types' +import type { ResolveV3, ResolveV4 } from './utils' import type { Strategies, I18nRoutingOptions } from '../types' import type { Locale } from '@intlify/vue-i18n-bridge' -import type { - Route, - RawLocation, - RouteLocation, - RouteLocationRaw, - RouteLocationNormalizedLoaded, - Router, - RouteMeta -} from '@intlify/vue-router-bridge' const RESOLVED_PREFIXED = new Set(['prefix_and_default', 'prefix_except_default']) @@ -90,8 +94,8 @@ export function localePath( // prettier-ignore return localizedRoute == null ? '' - : isVue3 - ? localizedRoute.redirectedFrom || localizedRoute.fullPath + : isV4Route(localizedRoute) + ? localizedRoute.redirectedFrom?.fullPath || localizedRoute.fullPath : localizedRoute.route.redirectedFrom || localizedRoute.route.fullPath } @@ -118,9 +122,9 @@ export function localeRoute( // prettier-ignore return resolved == null ? undefined - : isVue3 - ? resolved as ReturnType - : resolved.route as Route + : isV4Route(resolved) + ? resolved + : resolved.route } /** @@ -141,50 +145,55 @@ export function localeLocation( this: RoutingProxy, route: RawLocation | RouteLocationRaw, locale?: Locale // TODO: locale should be more type inference (completion) -): Location | RouteLocation | undefined { +): Location | (RouteLocation & { href: string }) | undefined { const resolved = resolveRoute.call(this, route, locale) // prettier-ignore return resolved == null ? undefined - : isVue3 + : isV4Route(resolved) ? resolved : resolved.location } -/* eslint-disable @typescript-eslint/no-explicit-any */ -export function resolveRoute(this: RoutingProxy, route: any, locale?: Locale): any { +export function resolveRoute(this: RoutingProxy, route: RawLocation | RouteLocationRaw, locale?: Locale) { const router = this.router const i18n = this.i18n - // console.log('resolveRoute', i18n.locale, Object.keys(i18n)) const _locale = locale || getLocale(i18n) const { routesNameSeparator, defaultLocale, defaultLocaleRouteNameSuffix, strategy, trailingSlash, prefixable } = getI18nRoutingOptions(router, this) // if route parameter is a string, check if it's a path or name of route. - let _route = route + let _route: RouteLocationPathRaw | RouteLocationNamedRaw if (isString(route)) { - if (_route[0] === '/') { + if (route[0] === '/') { // if route parameter is a path, create route object with path. - const [path, search] = route.split('?') - const query = Object.fromEntries(new URLSearchParams(search)) - _route = { path, query } + const { pathname: path, search, hash } = parsePath(route) + const query = parseQuery(search) + _route = { path, query, hash } } else { // else use it as route name. _route = { name: route } } + } else { + _route = route } - let localizedRoute = assign({}, _route) + let localizedRoute = assign({} as RouteLocationPathRaw | RouteLocationNamedRaw, _route) + + const isRouteLocationPathRaw = (val: RouteLocationPathRaw | RouteLocationNamedRaw): val is RouteLocationPathRaw => + 'path' in val && !!val.path && !('name' in val) - if (localizedRoute.path && !localizedRoute.name) { + if (isRouteLocationPathRaw(localizedRoute)) { let _resolvedRoute = null try { _resolvedRoute = resolve(router, localizedRoute, strategy, _locale) } catch {} + // prettier-ignore const resolvedRoute = isVue3 - ? _resolvedRoute // for vue-router v4 - : _resolvedRoute.route // for vue-router v3 + ? _resolvedRoute as ResolveV4 // for vue-router v4 + : (_resolvedRoute as ResolveV3).route // for vue-router v3 + const resolvedRouteName = getRouteBaseName.call(this, resolvedRoute) if (isString(resolvedRouteName)) { localizedRoute = { @@ -197,21 +206,24 @@ export function resolveRoute(this: RoutingProxy, route: any, locale?: Locale): a params: resolvedRoute.params, query: resolvedRoute.query, hash: resolvedRoute.hash - } + } as RouteLocationNamedRaw + if (isVue3) { - localizedRoute.state = resolvedRoute.state + // @ts-expect-error + localizedRoute.state = (resolvedRoute as ResolveV4).state } } else { // if route has a path defined but no name, resolve full route using the path if (prefixable({ currentLocale: _locale, defaultLocale, strategy })) { localizedRoute.path = `/${_locale}${localizedRoute.path}` } + localizedRoute.path = trailingSlash ? withTrailingSlash(localizedRoute.path, true) : withoutTrailingSlash(localizedRoute.path, true) } } else { - if (!localizedRoute.name && !localizedRoute.path) { + if (!localizedRoute.name && !('path' in localizedRoute)) { localizedRoute.name = getRouteBaseName.call(this, this.route) } @@ -231,26 +243,28 @@ export function resolveRoute(this: RoutingProxy, route: any, locale?: Locale): a } try { - const resolvedRoute = resolvedRouteToObject(router.resolve(localizedRoute)) - // prettier-ignore - if (isV4Route(resolvedRoute) - ? resolvedRoute.name // for vue-router v4 - : resolvedRoute.route.name // for vue-router v3 + const resolvedRoute = router.resolve(localizedRoute) + if ( + isV4Route(resolvedRoute) + ? resolvedRoute.name // for vue-router v4 + : resolvedRoute.route.name // for vue-router v3 ) { return resolvedRoute } + // if didn't resolve to an existing route then just return resolved route based on original input. - return (router as Router).resolve(route) - } catch (e: any) { - if (isVue3 && e.type === 1) { - // `1` is No match + return router.resolve(route) + } catch (e: unknown) { + if (isVue2) { return null - } else if (isVue2) { + } + + if (isVue3 && typeof e === 'object' && 'type' in e! && e.type === 1) { + // `1` is No match return null } } } -/* eslint-enable @typescript-eslint/no-explicit-any */ export const DefaultSwitchLocalePathIntercepter: SwitchLocalePathIntercepter = (path: string) => path diff --git a/packages/vue-i18n-routing/src/compatibles/utils.ts b/packages/vue-i18n-routing/src/compatibles/utils.ts index a5e5a30..9221af4 100644 --- a/packages/vue-i18n-routing/src/compatibles/utils.ts +++ b/packages/vue-i18n-routing/src/compatibles/utils.ts @@ -1,4 +1,4 @@ -import { assign, isArray } from '@intlify/shared' +import { assign } from '@intlify/shared' import { isVue3 } from 'vue-demi' import { @@ -18,7 +18,13 @@ import type { RoutingProxy } from './types' import type { I18nRoutingGlobalOptions } from '../extends/router' import type { Strategies } from '../types' import type { Locale } from '@intlify/vue-i18n-bridge' -import type { VueRouter, Router, Route, RouteLocationNormalizedLoaded } from '@intlify/vue-router-bridge' +import type { + VueRouter, + Router, + Route, + RouteLocationNormalizedLoaded, + RouteLocationPathRaw +} from '@intlify/vue-router-bridge' export function getI18nRoutingOptions( router: Router | VueRouter, @@ -78,43 +84,18 @@ export function routeToObject(route: Route | RouteLocationNormalizedLoaded) { } } -/** - * This function maps the response of `router.resolve` to properly encode the path. - * - * @param route - the {@link RouteLocation} provided by `router.resolve`. - * @returns a {@link RouteLocation} with URL encoded `fullPath`, `path` and `href` properties. - */ -type ResolvedRoute = ReturnType | ReturnType['route'] -type BridgeRoute = ResolvedRoute | { route: ResolvedRoute } +export type ResolveV3 = ReturnType +export type ResolveV4 = ReturnType +type ResolvedRoute = ResolveV3 | ResolveV4 -export function isV4Route(val: BridgeRoute): val is ResolvedRoute { +export function isV4Route(val: ResolvedRoute): val is ReturnType { return isVue3 } -export function resolveBridgeRoute(val: BridgeRoute) { - return isV4Route(val) ? val : val.route +export function isV4Router(val: Router | VueRouter): val is Router { + return isVue3 } -/** - * This function maps the response of `router.resolve` to properly encode the path. - * - * @param route - the {@link BridgeRoute} provided by `router.resolve`. - * @returns a {@link BridgeRoute} with URL encoded `fullPath`, `path` and `href` properties. - */ -export function resolvedRouteToObject(route: BridgeRoute): BridgeRoute { - const r = resolveBridgeRoute(route) - - const encodedPath = encodeURI(r.path) - const queryString = r.fullPath.indexOf('?') >= 0 ? r.fullPath.substring(r.fullPath.indexOf('?')) : '' - const resolvedObject = { - ...r, - fullPath: encodedPath + queryString, - path: encodedPath, - href: encodedPath + queryString - } - - return isVue3 ? resolvedObject : { ...route, route: resolvedObject } -} /** * NOTE: * vue-router v4.x `router.resolve` for a non exists path will output a warning. @@ -122,25 +103,28 @@ export function resolvedRouteToObject(route: BridgeRoute): BridgeRoute { * When using the `prefix` strategy, the path specified by `localePath` is specified as a path not prefixed with a locale. * This will cause vue-router to issue a warning, so we can work-around by using `router.options.routes`. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function resolve(router: Router | VueRouter, route: any, strategy: Strategies, locale: Locale): any { - if (isVue3 && strategy === 'prefix') { - if (isArray(route.matched) && route.matched.length > 0) { - return route.matched[0] - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [rootSlash, restPath] = split(route.path, 1) - const targetPath = `${rootSlash}${locale}${restPath === '' ? restPath : `/${restPath}`}` - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const _route = (router as any).options.routes.find((r: any) => r.path === targetPath) - if (_route == null) { - return route - } else { - const _resolvableRoute = assign({}, route, _route) - _resolvableRoute.path = targetPath - return router.resolve(_resolvableRoute) - } - } else { +export function resolve(router: Router | VueRouter, route: RouteLocationPathRaw, strategy: Strategies, locale: Locale) { + if (isV4Router(router)) { return router.resolve(route) } + + if (strategy !== 'prefix') { + return router.resolve(route) + } + + // if (isArray(route.matched) && route.matched.length > 0) { + // return route.matched[0] + // } + + const [rootSlash, restPath] = split(route.path, 1) + const targetPath = `${rootSlash}${locale}${restPath === '' ? restPath : `/${restPath}`}` + const _route = router.options?.routes?.find(r => r.path === targetPath) + + if (_route == null) { + return router.resolve(route) + } + + const _resolvableRoute = assign({}, route, _route) + _resolvableRoute.path = targetPath + return router.resolve(_resolvableRoute) } diff --git a/packages/vue-i18n-routing/src/utils.ts b/packages/vue-i18n-routing/src/utils.ts index 2c132a1..b10eee7 100644 --- a/packages/vue-i18n-routing/src/utils.ts +++ b/packages/vue-i18n-routing/src/utils.ts @@ -194,7 +194,7 @@ export function getRouteName(routeName?: string | symbol | null) { } export function getLocaleRouteName( - routeName: string | null, + routeName: symbol | string | null | undefined, locale: Locale, { defaultLocale,