Skip to content

Commit

Permalink
Merge pull request #59 from kitbagjs/router-push-types
Browse files Browse the repository at this point in the history
  • Loading branch information
pleek91 authored Jan 21, 2024
2 parents ace4431 + 0ab95ba commit 58d52fe
Show file tree
Hide file tree
Showing 11 changed files with 105 additions and 74 deletions.
1 change: 0 additions & 1 deletion src/types/flattened.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ type RouteFlat<
? Record<Prefix<Name, TPrefix>, MarkOptionalParams<MergeParams<RoutePathParams<TRoute, TPathParams>, RouteQueryParams<TRoute, TQueryParams>>>>
: Record<never, never>


type RouteChildrenFlat<
TRoute extends Route,
TPrefix extends string,
Expand Down
8 changes: 4 additions & 4 deletions src/types/routeMethods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ export type ExtractRouteMethodParams<T extends RouteMethod> =
export type RoutePathParams<
TRoute extends Route,
TPathParams extends Record<string, unknown>
> = TRoute extends { path: infer Path }
? MergeParams<TPathParams, ExtractParamsFromPath<Path>>
> = TRoute extends { path: infer TPath extends string | Path }
? MergeParams<TPathParams, ExtractParamsFromPath<TPath>>
: MergeParams<TPathParams, {}>

export type RouteQueryParams<
TRoute extends Route,
TQueryParams extends Record<string, unknown>
> = TRoute extends { query: infer Query }
? MergeParams<TQueryParams, ExtractParamsFromQuery<Query>>
> = TRoute extends { query: infer TQuery extends string | Query }
? MergeParams<TQueryParams, ExtractParamsFromQuery<TQuery>>
: MergeParams<TQueryParams, {}>

type ExtractParamsFromPath<
Expand Down
30 changes: 19 additions & 11 deletions src/types/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Flattened } from '@/types/flattened'
import { Resolved } from '@/types/resolved'
import { RouteMethods } from '@/types/routeMethods'
import { Route, Routes } from '@/types/routes'
import { AllPropertiesAreOptional, Identity } from '@/types/utilities'

export type RouterOptions = {
initialUrl?: string,
Expand All @@ -14,19 +15,26 @@ export type RouterPushOptions = {
replace?: boolean,
}

type RouterPushRoute<
TRoutes extends Routes,
TRoute extends PropertyKey
> = RouterPushOptions & {
name: TRoute,
params?: TRoute extends keyof Flattened<TRoutes> ? Flattened<TRoutes>[TRoute] : Record<never, never>,
}
type RoutePushConfigParams<
TParams
> = TParams extends Record<string, unknown>
? AllPropertiesAreOptional<TParams> extends true
? { params?: TParams }
: { params: TParams }
: {}

export type RouterPushUrl = (url: string, options?: RouterPushOptions) => Promise<void>

export type RouterPush<TRoutes extends Routes = Routes> = {
export type RouterPushConfig<
TRoutes extends Routes,
TFlat = Flattened<TRoutes>
> = Identity<{
[Route in keyof TFlat]: {
route: Route,
} & RoutePushConfigParams<TFlat[Route]>
}[keyof TFlat] & RouterPushOptions>

export type RouterPush<TRoutes extends Routes> = {
(url: string, options?: RouterPushOptions): Promise<void>,
<TRoute extends keyof Flattened<TRoutes>>(route: RouterPushRoute<TRoutes, TRoute>): Promise<void>,
(route: RouterPushConfig<TRoutes>): Promise<void>,
}

export type RouterReplaceOptions = Omit<RouterPushOptions, 'replace'>
Expand Down
5 changes: 3 additions & 2 deletions src/types/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ export type ChildRoute<
}

export type Route<
TRoute extends string | Path = any
> = ParentRoute<TRoute> | ChildRoute<TRoute>
TPath extends string | Path = any,
TQuery extends string | Query = any
> = ParentRoute<TPath, TQuery> | ChildRoute<TPath, TQuery>

export type Routes = Readonly<Route[]>

Expand Down
4 changes: 3 additions & 1 deletion src/types/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,6 @@ export type ReplaceAll<

export type OnlyRequiredProperties<T extends Record<string, unknown>> = {
[K in keyof T as Extract<T[K], undefined> extends never ? K : never]: T[K]
}
}

export type AllPropertiesAreOptional<T extends Record<string, unknown>> = IsEmptyObject<OnlyRequiredProperties<T>>
32 changes: 16 additions & 16 deletions src/utilities/createRouteMethods.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ test.each([
[undefined],
[true],
])('given route is named and is public, makes parent callable', (isPublic) => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -19,7 +19,7 @@ test.each([
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods<typeof routes>(resolved, routerPush)
const response = createRouteMethods<typeof routes>({ resolved, push })

if (isPublic !== false) {
// @ts-expect-error
Expand All @@ -32,7 +32,7 @@ test.each([
})

test('given route is NOT public, returns empty object', () => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -44,7 +44,7 @@ test('given route is NOT public, returns empty object', () => {
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods<typeof routes>(resolved, routerPush)
const response = createRouteMethods<typeof routes>({ resolved, push })

expect(response).toMatchObject({})
})
Expand All @@ -54,7 +54,7 @@ test.each([
[true],
[false],
])('given parent route with named children, has property for child name', (isPublic) => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -72,7 +72,7 @@ test.each([
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods<typeof routes>(resolved, routerPush)
const response = createRouteMethods<typeof routes>({ resolved, push })

if (isPublic !== false) {
// @ts-expect-error
Expand All @@ -84,7 +84,7 @@ test.each([
})

test('given parent route with named children and grandchildren, has path to grandchild all callable', () => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -107,7 +107,7 @@ test('given parent route with named children and grandchildren, has path to gran
] as const satisfies Routes
const resolved = resolveRoutes(routes)

const response = createRouteMethods<typeof routes>(resolved, routerPush)
const response = createRouteMethods<typeof routes>({ resolved, push })

expect(response.parent).toBeTypeOf('function')
expect(response.parent.child).toBeTypeOf('function')
Expand All @@ -116,7 +116,7 @@ test('given parent route with named children and grandchildren, has path to gran

describe('routeMethod', () => {
test('push and replace call router.push with correct parameters', () => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -126,23 +126,23 @@ describe('routeMethod', () => {
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)
const { route } = createRouteMethods<typeof routes>(resolved, routerPush)
const { route } = createRouteMethods<typeof routes>({ resolved, push })

route().push()
expect(routerPush).toHaveBeenLastCalledWith('/route/', {})
expect(push).toHaveBeenLastCalledWith('/route/', {})

route().replace()
expect(routerPush).toHaveBeenLastCalledWith('/route/', { replace: true })
expect(push).toHaveBeenLastCalledWith('/route/', { replace: true })

route({ param: 'foo' }).push()
expect(routerPush).toHaveBeenLastCalledWith('/route/foo', {})
expect(push).toHaveBeenLastCalledWith('/route/foo', {})

route({ param: 'foo' }).push({ params: { param: 'bar' } })
expect(routerPush).toHaveBeenLastCalledWith('/route/bar', {})
expect(push).toHaveBeenLastCalledWith('/route/bar', {})
})

test('returns correct url', () => {
const routerPush = vi.fn()
const push = vi.fn()

const routes = [
{
Expand All @@ -152,7 +152,7 @@ describe('routeMethod', () => {
},
] as const satisfies Routes
const resolved = resolveRoutes(routes)
const { route } = createRouteMethods<typeof routes>(resolved, routerPush)
const { route } = createRouteMethods<typeof routes>({ resolved, push })

expect(route().url).toBe('/route/')
expect(route({ param: 'param' }).url).toBe('/route/param')
Expand Down
20 changes: 12 additions & 8 deletions src/utilities/createRouteMethods.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { Resolved, Route, RouteMethods, Routes, isPublicRoute } from '@/types'
import { Resolved, Route, RouteMethods, RouterPush, Routes, isPublicRoute } from '@/types'
import { RouteMethod, RouteMethodPush, RouteMethodReplace } from '@/types/routeMethod'
import { RouterPushUrl } from '@/types/router'
import { asArray } from '@/utilities/array'
import { assembleUrl } from '@/utilities/urlAssembly'

export function createRouteMethods<T extends Routes = Routes>(routes: Resolved<Route>[], routerPush: RouterPushUrl): RouteMethods<T> {
const methods = routes.reduce<Record<string, any>>((methods, route) => {
type RouteMethodsContext<T extends Routes> = {
resolved: Resolved<Route>[],
push: RouterPush<T>,
}

export function createRouteMethods<T extends Routes>({ resolved, push }: RouteMethodsContext<T>): RouteMethods<T> {
const methods = resolved.reduce<Record<string, any>>((methods, route) => {
let level = methods

route.matches.forEach(match => {
Expand All @@ -16,7 +20,7 @@ export function createRouteMethods<T extends Routes = Routes>(routes: Resolved<R
const isLeaf = match === route.matched

if (isLeaf && isPublicRoute(route.matched)) {
const method = createRouteMethod({ route, routerPush })
const method = createRouteMethod<T>({ route, push })

level[route.name] = Object.assign(method, level[route.name])
return
Expand All @@ -35,12 +39,12 @@ export function createRouteMethods<T extends Routes = Routes>(routes: Resolved<R
return methods as any
}

type CreateRouteMethodArgs = {
type CreateRouteMethodArgs<T extends Routes> = {
route: Resolved<Route>,
routerPush: RouterPushUrl,
push: RouterPush<T>,
}

function createRouteMethod({ route, routerPush }: CreateRouteMethodArgs): RouteMethod {
function createRouteMethod<T extends Routes>({ route, push: routerPush }: CreateRouteMethodArgs<T>): RouteMethod {
const node: RouteMethod = (params = {}) => {
const normalizedParams = normalizeRouteParams(params)
const url = assembleUrl(route, normalizedParams)
Expand Down
2 changes: 1 addition & 1 deletion src/utilities/createRouter.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ test('updates the route when navigating', async () => {

expect(route.matched).toMatchObject(second)

await push({ name: third.name, params: { id: '123' } })
await push({ route: third.name, params: { id: '123' } })

expect(route.matched).toMatchObject(third)
})
37 changes: 8 additions & 29 deletions src/utilities/createRouter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { reactive, readonly, App, InjectionKey } from 'vue'
import { RouterLink, RouterView } from '@/components'
import { Resolved, Route, Routes, Router, RouterOptions, RouterPushOptions, RegisteredRouter, RouterReplaceOptions, RouterPush } from '@/types'
import { createRouteMethods, createRouterNavigation, resolveRoutes, routeMatch, getInitialUrl, resolveRoutesRegex, assembleUrl, flattenParentMatches } from '@/utilities'
import { Resolved, Route, Routes, Router, RouterOptions, RegisteredRouter, RouterReplaceOptions } from '@/types'
import { createRouteMethods, createRouterNavigation, resolveRoutes, routeMatch, getInitialUrl, resolveRoutesRegex } from '@/utilities'
import { createRouterPush } from '@/utilities/createRouterPush'

export const routerInjectionKey: InjectionKey<RegisteredRouter> = Symbol()

Expand Down Expand Up @@ -43,38 +44,16 @@ export function createRouter<T extends Routes>(routes: T, options: RouterOptions
Object.assign(route, newRoute)
}

function pushUrl(url: string, options: RouterPushOptions = {}): Promise<void> {
return navigation.update(url, options)
function replace(url: string, options: RouterReplaceOptions = {}): Promise<void> {
return push(url, { ...options, replace: true })
}

function pushRoute({ name, params, replace }: { name: string, params?: Record<string, any> } & RouterPushOptions): Promise<void> {
const match = resolved.find((route) => flattenParentMatches(route) === name)

if (!match) {
throw `No route found with name "${String(name)}"`
}

const url = assembleUrl(match, params)

return navigation.update(url, { replace })
}

function push(urlOrRoute: string | { name: string, params?: Record<string, any> } & RouterPushOptions, possiblyOptions: RouterPushOptions = {}): Promise<void> {
if (typeof urlOrRoute === 'string') {
return pushUrl(urlOrRoute, possiblyOptions)
}

return pushRoute(urlOrRoute)
}

async function replace(url: string, options: RouterReplaceOptions = {}): Promise<void> {
await navigation.update(url, { ...options, replace: true })
}
const push = createRouterPush<T>({ navigation, resolved })

const router = {
routes: createRouteMethods<T>(resolved, pushUrl),
routes: createRouteMethods<T>({ resolved, push }),
route: readonly(route),
push: push as RouterPush<T>,
push,
replace,
forward: navigation.forward,
back: navigation.back,
Expand Down
38 changes: 38 additions & 0 deletions src/utilities/createRouterPush.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Resolved, Route, RouteComponent, RouterPush, RouterPushOptions, Routes } from '@/types'
import { flattenParentMatches } from '@/utilities/flattenParentMatches'
import { RouterNavigation } from '@/utilities/routerNavigation'
import { assembleUrl } from '@/utilities/urlAssembly'

type AnyRoutes = [{ name: string, path: string, component: RouteComponent }]

type RouterPushContext = {
navigation: RouterNavigation,
resolved: Resolved<Route>[],
}

export function createRouterPush<const TRoutes extends Routes>({ navigation, resolved }: RouterPushContext): RouterPush<TRoutes> {

const push: RouterPush<AnyRoutes> = (urlOrRouteConfig, options?: RouterPushOptions) => {
if (typeof urlOrRouteConfig === 'object') {
const { route, params, ...options } = urlOrRouteConfig
const match = resolved.find((resolvedRoute) => flattenParentMatches(resolvedRoute) === route)

if (!match) {
throw `No route found: "${String(route)}"`
}

const url = assembleUrl(match, params)

return push(url, options)
}

if (typeof urlOrRouteConfig === 'string') {
return navigation.update(urlOrRouteConfig, options)
}

const exhaustive: never = urlOrRouteConfig
throw new Error(`Unhandled router push overload: ${JSON.stringify(exhaustive)}`)
}

return push as any
}
2 changes: 1 addition & 1 deletion src/utilities/routerNavigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ type NavigationGo = (delta: number) => void
type NavigationUpdate = (url: string, options?: RouterNavigationUpdateOptions) => Promise<void>
type NavigationCleanup = () => void

type RouterNavigation = {
export type RouterNavigation = {
forward: NavigationForward,
back: NavigationBack,
go: NavigationGo,
Expand Down

0 comments on commit 58d52fe

Please sign in to comment.