Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More complete types for RouterPush and abstact implementation into createRouterPush #59

Merged
merged 3 commits into from
Jan 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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<
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type just makes sure the params property is not required if there are no required params.

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 })
Comment on lines +47 to +48
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated replace to just call router.push. That way theres only one implementation of what "replace" means.

}

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