import tc from '@spaceavocado/type-check' ; import pathToRegexp from 'path-to-regexp' ; import createHistory , { HISTORY_MODE , HISTORY_ACTION , HASH_TYPE , } from './history' ; import { Location , RawLocation , createLocation , } from './location' ; import { HistoryLocation , joinPath , fullURL , historyFullURL , hasPrefix , trimPrefix , } from './utils' ; import { Route , Record , RouteConfig , RouteConfigPrefab , routeRedirect , componentModule , createRouteConfig , createRouteRecord , createRoute , cloneRoute , } from './route' ; type historyModule = { action : HISTORY_ACTION ; location : HistoryLocation ; push : ( path : string ) => void ; replace : ( path : string ) => void ; go : ( n : number ) => void ; goBack : ( ) => void ; goForward : ( ) => void ; listen : ( listener : ( location : HistoryLocation , action : HISTORY_ACTION ) => void ) => ( ) => void ; } ; /**
 * History lib configuration.
 */ interface HistoryOptions { /** The base URL of the app, defaults to ''. */ basename ? : string ; /** Hash type. */ hashType ? : string ; } /**
 * Router configuration.
 */ export interface RouterConfig { /** History mode */ mode ? : HISTORY_MODE ; /** The base URL of the app, defaults to ''. */ basename ? : string ; /** Hash type. */ hashType ? : HASH_TYPE ; /** Router routes. */ routes : RouteConfigPrefab [ ] ; /** CSS class applied on the active route link. Defaults to "active". */ activeClass ? : string ; /** History options */ historyOpts ? : HistoryOptions ; } /**
 * Possible actions:
 * * fn() or fn(true) = Continue.
 * * fn(false) = Abort the navigation.
 * * fn(Error) = Abort the navigation and trigger navigation error.
 * * fn(url) or fn(RawLocation) = Break the navigation
 * and resolve the new navigation.
 */ type navigationGuardNextAction = undefined | boolean | Error | string | object ; type navigationGuardFunction = ( from : Route | null , to : Route | null , next : ( action : navigationGuardNextAction ) => void ) => void ; /**
 * Navigation Guard entry.
 */ interface NavigationGuard { key : symbol ; guard : navigationGuardFunction ; } type onErrorCallback = ( e : Error ) => void ; type onNavigationCallback = ( from : Route , to : Route ) => void ; /**
 * Router event listeners collection.
 */ interface EventListeners { onError : Map < symbol , onErrorCallback > ; onBeforeNavigation : Map < symbol , onNavigationCallback > ; onNavigationChanged : Map < symbol , onNavigationCallback > ; } /**
 * Svelte Router core class.
 */ export class Router { private _mode : HISTORY_MODE ; private _basename : string ; private _routes : RouteConfig [ ] ; private _activeClass : string ; private _history : historyModule ; private _historyListener : ( ) => void ; private _navigationGuards : NavigationGuard [ ] ; private _listeners : EventListeners ; private _currentRoute : Route | null = null ; private _pendingRoute : Route | null = null ; private _asyncViews : Map < symbol , ( ) => object > ; constructor param { RouterConfig } opts constructor ( opts ) { opts = opts || { } ; opts . historyOpts = { } ; opts . mode = opts . mode || HISTORY_MODE . HISTORY ; if ( tc . not . isEnumKey ( opts . mode , HISTORY_MODE ) ) { throw new Error ( `invalid router mode, "${ opts . mode }"` ) ; } opts . historyOpts . basename = opts . basename || '' ; if ( tc . not . isString ( opts . historyOpts . basename ) ) { throw new Error ( `invalid basename, "${ opts . historyOpts . basename }"` ) ; } if ( opts . historyOpts . basename . length > 0 && hasPrefix ( opts . historyOpts . basename , '/' ) == false ) { opts . historyOpts . basename = '/' + opts . historyOpts . basename ; } if ( opts . mode == HISTORY_MODE . HASH ) { opts . historyOpts . hashType = opts . hashType || HASH_TYPE . SLASH ; if ( tc . not . isEnumKey ( opts . historyOpts . hashType , HASH_TYPE ) ) { throw new Error ( `invalid hash type, "${ opts . historyOpts . hashType }"` ) ; } opts . historyOpts . hashType = opts . historyOpts . hashType . toLowerCase ( ) ; } this . _mode = opts . mode ; this . _basename = opts . historyOpts . basename ; this . _routes = [ ] ; this . _activeClass = opts . activeClass || 'active' ; this . _history = createHistory ( this . _mode , opts . historyOpts || { } ) as historyModule ; this . _historyListener = this . _history . listen ( this . onHistoryChange . bind ( this ) ) ; this . _navigationGuards = [ ] ; this . _listeners = { onError : new Map ( ) , onBeforeNavigation : new Map ( ) , onNavigationChanged : new Map ( ) , } ; this . _currentRoute = null ; this . _pendingRoute = null ; this . _asyncViews = new Map ( ) ; this . preprocessRoutes ( this . _routes , opts . routes ) ; } /**
     * Trigger the on load history change.
     */ start ( ) { this . onHistoryChange ( this . _history . location , HISTORY_ACTION . POP ) ; } /**
     * Get router mode
     */ get mode ( ) : HISTORY_MODE { return this . _mode ; } /**
     * Get router basename
     */ get basename ( ) : string { return this . _basename ; } /**
     * Get routes
     */ get routes ( ) : RouteConfig [ ] { return this . _routes ; } /**
     * Get current resolved route
     */ get currentRoute ( ) : Route | null { return this . _currentRoute ; } /**
     * Get router link active class
     */ get activeClass ( ) : string { return this . _activeClass ; } param { function } guard return { function } navigationGuard ( guard ) { const key = Symbol ( ) ; this . _navigationGuards . push ( { key , guard , } ) ; return ( ) : void => { this . removeNavigationGuard ( key ) ; } ; } param { function } callback return { function } onBeforeNavigation ( callback ) { const key = Symbol ( ) ; this . _listeners . onBeforeNavigation . set ( key , callback ) ; return ( ) : void => { this . _listeners . onBeforeNavigation . delete ( key ) ; } ; } param { function } callback return { function } onNavigationChanged ( callback ) { const key = Symbol ( ) ; this . _listeners . onNavigationChanged . set ( key , callback ) ; return ( ) : void => { this . _listeners . onNavigationChanged . delete ( key ) ; } ; } param { function } callback return { function } onError ( callback ) { const key = Symbol ( ) ; this . _listeners . onError . set ( key , callback ) ; return ( ) : void => { this . _listeners . onError . delete ( key ) ; } ; } param { RawLocation | string } rawLocation param { function ? } onComplete param { function ? } onAbort throws push ( rawLocation , onComplete ? , onAbort ? ) { let location ; try { location = this . rawLocationToLocation ( rawLocation , false ) ; } catch ( e ) { if ( onAbort && tc . isFunction ( onAbort ) ) { onAbort ( ) ; } this . notifyOnError ( new Error ( `invalid location, ${ e . toString ( ) }` ) ) ; return ; } this . resolveRoute ( location , tc . isFunction ( onComplete ) ? onComplete : undefined , tc . isFunction ( onAbort ) ? onAbort : undefined ) ; } param { RawLocation | string } rawLocation param { function ? } onComplete param { function ? } onAbort throws replace ( rawLocation , onComplete ? , onAbort ? ) { let location ; try { location = this . rawLocationToLocation ( rawLocation , true ) ; } catch ( e ) { if ( onAbort && tc . isFunction ( onAbort ) ) { onAbort ( ) ; } this . notifyOnError ( new Error ( `invalid location, ${ e . toString ( ) }` ) ) ; return ; } this . resolveRoute ( location , tc . isFunction ( onComplete ) ? onComplete : undefined , tc . isFunction ( onAbort ) ? onAbort : undefined ) ; } param { number } n go ( n ) { this . _history . go ( n ) ; } /**
     * Go one step back in the navigation history.
     */ back ( ) { this . _history . goBack ( ) ; } /**
     * Go one step forward in the navigation history.
     */ forward ( ) { this . _history . goForward ( ) ; } param { RawLocation } rawLocation throws return { string } routeURL ( rawLocation ) { if ( tc . isNullOrUndefined ( rawLocation ) ) { throw new Error ( 'invalid rawLocation' ) ; } if ( tc . isNullOrUndefined ( rawLocation . name ) || tc . not . isString ( rawLocation . name ) ) { throw new Error ( 'missing or invalid route name' ) ; } if ( tc . not . isNullOrUndefined ( rawLocation . params ) && tc . not . isObject ( rawLocation . params ) ) { throw new Error ( 'invalid params property, expected object.' ) ; } if ( tc . not . isNullOrUndefined ( rawLocation . query ) && tc . not . isObject ( rawLocation . query ) ) { throw new Error ( 'invalid query property, expected object.' ) ; } if ( tc . not . isNullOrUndefined ( rawLocation . hash ) && tc . not . isString ( rawLocation . hash ) ) { throw new Error ( 'invalid hash property' ) ; } rawLocation . params = rawLocation . params || { } ; rawLocation . query = rawLocation . query || { } ; rawLocation . hash = rawLocation . hash || '' ; const match = this . findRouteByName ( rawLocation . name as string , this . _routes ) ; if ( match == null ) { throw new Error ( `no matching route found for name:${ rawLocation . name }` ) ; } let url = '' ; try { url = match . generator ( rawLocation . params || { } ) ; } catch ( e ) { throw new Error ( `invalid route parameters, :${ e . toString ( ) }` ) ; } url = fullURL ( url , rawLocation . query , rawLocation . hash ) ; if ( this . _basename . length > 0 ) { url = joinPath ( this . _basename , url ) ; } return url ; } param { RouteConfig [ ] } routes param { RouteConfigPrefab [ ] } prefabs param { RouteConfig | null } parent private preprocessRoutes ( routes , prefabs , parent = null ) { for ( let i = 0 ; i < prefabs . length ; i ++ ) { let route ; try { prefabs [ i ] . children = prefabs [ i ] . children || [ ] ; route = createRouteConfig ( prefabs [ i ] ) ; route . parent = null ; routes . push ( route ) ; } catch ( e ) { console . error ( new Error ( `invalid route, ${ e . toString ( ) }` ) ) ; continue ; } if ( parent != null ) { route . parent = parent ; if ( route . path . length > 0 ) { route . path = joinPath ( parent . path , route . path ) ; } else { route . path = parent . path ; } } route . paramKeys = [ ] ; if ( route . path == '*' ) { route . matcher = /.*/i ; route . generator = ( ) : string => '/' ; } else { route . matcher = pathToRegexp ( route . path , route . paramKeys as pathToRegexp . Key [ ] , { end : ( prefabs [ i ] . children as RouteConfigPrefab [ ] ) . length == 0 , } ) ; route . generator = pathToRegexp . compile ( route . path ) ; } if ( ( prefabs [ i ] . children as RouteConfigPrefab [ ] ) . length > 0 ) { this . preprocessRoutes ( route . children , prefabs [ i ] . children as RouteConfigPrefab [ ] , route ) ; } } } param { HistoryLocation } location param { HISTORY_ACTION } action private onHistoryChange ( location , action ) { if ( action == HISTORY_ACTION . POP ) { this . push ( historyFullURL ( location ) ) ; } } param { RawLocation | string } rawLocation param { boolean } replace throws return { Location } private rawLocationToLocation ( rawLocation , replace ) { if ( tc . isNullOrUndefined ( rawLocation ) ) { throw new Error ( 'invalid rawLocation' ) ; } if ( tc . isString ( rawLocation ) ) { rawLocation = { path : rawLocation , } as RawLocation ; } rawLocation . replace = replace ; let location ; try { location = createLocation ( rawLocation as RawLocation ) ; } catch ( e ) { throw e ; } return location ; } param { Location } location param { function ? } onComplete param { function ? } onAbort private resolveRoute ( location , onComplete ? , onAbort ? ) { let matches = [ ] ; if ( this . _basename . length > 0 ) { location . path = trimPrefix ( location . path , this . _basename ) ; } if ( location . name ) { let match = this . findRouteByName ( location . name , this . _routes ) ; if ( match == null ) { if ( onAbort != null ) { onAbort ( ) ; } this . notifyOnError ( new Error ( `no matching route found for name:${ location . name }` ) ) ; return ; } try { location . path = match . generator ( location . params ) ; } catch ( e ) { if ( onAbort != null ) { onAbort ( ) ; } this . notifyOnError ( new Error ( `invalid route parameters, :${ e . toString ( ) }` ) ) ; return ; } matches . push ( createRouteRecord ( match , location . params ) ) ; while ( match . parent != null ) { match = match . parent ; matches . push ( createRouteRecord ( match , location . params ) ) ; } if ( matches . length > 1 ) { matches = matches . reverse ( ) ; } } else { if ( this . matchRoute ( location . path , this . _routes , matches ) == false ) { if ( onAbort != null ) { onAbort ( ) ; } this . notifyOnError ( new Error ( `no matching route found for path:${ location . path }` ) ) ; return ; } } this . _pendingRoute = createRoute ( location , matches ) ; if ( this . _pendingRoute . redirect != null ) { this . resolveRedirect ( this . _pendingRoute . redirect , onComplete , onAbort ) ; return ; } if ( this . _currentRoute && this . _pendingRoute . fullPath == this . _currentRoute . fullPath ) { this . _pendingRoute = null ; if ( onComplete != null ) { onComplete ( ) ; } return ; } Object . freeze ( this . _currentRoute ) ; Object . freeze ( this . _pendingRoute ) ; this . notifyOnBeforeNavigation ( Object . freeze ( cloneRoute ( this . _currentRoute as Route ) ) , Object . freeze ( cloneRoute ( this . _pendingRoute as Route ) ) ) ; this . resolveNavigationGuard ( 0 , onComplete , onAbort ) ; } param { string } path param { RouteConfig [ ] } routes param { Record [ ] } matches return { boolean } private matchRoute ( path , routes , matches ) { for ( let i = 0 ; i < routes . length ; i ++ ) { const match = routes [ i ] . matcher . exec ( path ) ; if ( match ) { matches . push ( createRouteRecord ( routes [ i ] , match ) ) ; if ( routes [ i ] . children . length == 0 ) { return true ; } if ( this . matchRoute ( path , routes [ i ] . children , matches ) ) { return true ; } else { matches . pop ( ) ; } } } return false ; } param { string } name param { RouteConfig [ ] } routes return { RouteConfig | null } private findRouteByName ( name , routes ) { for ( let i = 0 ; i < routes . length ; i ++ ) { if ( routes [ i ] . name == name ) { return routes [ i ] ; } const match = this . findRouteByName ( name , routes [ i ] . children ) ; if ( match != null ) { return match ; } } return null ; } param { function | object | string } redirect param { function ? } onComplete param { function ? } onAbort private resolveRedirect ( redirect , onComplete , onAbort ) { if ( tc . isFunction ( redirect ) ) { redirect = ( redirect as ( to : Route ) => string ) ( this . _pendingRoute as Route ) ; } if ( tc . isString ( redirect ) && hasPrefix ( redirect as string , 'http' ) ) { window . location . replace ( redirect as string ) ; return ; } this . _pendingRoute = null ; this . push ( tc . isString ( redirect ) ? redirect as string : redirect as RawLocation , onComplete , onAbort ) ; } param { number } index param { function ? } onComplete param { function ? } onAbort private resolveNavigationGuard ( index = 0 , onComplete , onAbort ) { if ( index >= this . _navigationGuards . length ) { this . finishNavigationChange ( onComplete , onAbort ) ; return ; } const abort = ( err = null ) : void => { this . _pendingRoute = null ; if ( onAbort ) { onAbort ( ) ; } if ( err != null ) { this . notifyOnError ( new Error ( `navigation guard error, ${ err . toString ( ) }` ) ) ; } if ( this . _currentRoute != null && ( historyFullURL ( this . _history . location ) != this . _currentRoute . fullPath ) ) { this . _history . push ( this . _currentRoute . fullPath ) ; } } ; this . _navigationGuards [ index ] . guard ( this . _currentRoute , this . _pendingRoute , ( next ) => { if ( next == undefined ) { this . resolveNavigationGuard ( ++ index , onComplete , onAbort ) ; } else if ( next === false ) { abort ( ) ; } else if ( tc . isError ( next ) ) { abort ( next as Error ) ; } else if ( tc . isString ( next ) || tc . isObject ( next ) ) { this . _pendingRoute = null ; this . push ( next as string , onComplete , onAbort ) ; } else { abort ( new Error ( `unexpected next(val) value.` ) ) ; } } ) ; } param { Error } error private notifyOnError ( error ) { for ( const callback of this . _listeners . onError . values ( ) ) { callback ( error ) ; } } param { Route } from param { Route } to private notifyOnBeforeNavigation ( from , to ) { for ( const callback of this . _listeners . onBeforeNavigation . values ( ) ) { callback ( from , to ) ; } } param { Route } from param { Route } to private notifyOnNavigationChanged ( from , to ) { for ( const callback of this . _listeners . onNavigationChanged . values ( ) ) { callback ( from , to ) ; } } param { function ? } onComplete param { function ? } onAbort private finishNavigationChange ( onComplete ? , onAbort ? ) { if ( this . _pendingRoute == null ) { throw new Error ( 'navigation cannot be finished, missing pending route' ) ; } const asyncPending = [ ] ; for ( const r of this . _pendingRoute . matched ) { if ( r . async == false ) { continue ; } if ( this . _asyncViews . has ( r . id ) == false ) { asyncPending . push ( new Promise ( ( resolve , reject ) : void => { ( r . component as Promise < componentModule > ) . then ( ( m ) => resolve ( { id : r . id , component : m . default } ) ) . catch ( ( e ) => reject ( e ) ) ; } ) ) ; } } const afterResolved = ( ) : void => { if ( this . _pendingRoute == null ) { throw new Error ( 'navigation cannot be finished, missing pending route' ) ; } for ( const r of this . _pendingRoute . matched ) { if ( r . async == false ) { continue ; } r . component = this . _asyncViews . get ( r . id ) as ( ) => object ; } this . notifyOnNavigationChanged ( Object . freeze ( cloneRoute ( this . _currentRoute as Route ) ) , Object . freeze ( cloneRoute ( this . _pendingRoute as Route ) ) ) ; this . _currentRoute = cloneRoute ( this . _pendingRoute as Route ) ; this . _pendingRoute = null ; if ( historyFullURL ( this . _history . location ) != this . _currentRoute . fullPath ) { if ( this . _currentRoute . action == HISTORY_ACTION . PUSH ) { this . _history . push ( this . _currentRoute . fullPath ) ; } else if ( this . _currentRoute . action == HISTORY_ACTION . REPLACE ) { this . _history . replace ( this . _currentRoute . fullPath ) ; } } if ( onComplete != null ) { onComplete ( ) ; } } ; if ( asyncPending . length > 0 ) { Promise . all ( asyncPending ) . then ( ( views ) => { for ( const v of views ) { const view = v as { id : symbol ; component : ( ) => object ; } ; this . _asyncViews . set ( view . id , view . component ) ; } afterResolved ( ) ; } ) . catch ( ( e ) => { this . notifyOnError ( new Error ( `failed to load async error, ${ e . toString ( ) }` ) ) ; if ( onAbort != null ) { onAbort ( ) ; } } ) ; } else { afterResolved ( ) ; } } param { symbol } key private removeNavigationGuard ( key ) { for ( let i = 0 ; i < this . _navigationGuards . length ; i ++ ) { if ( this . _navigationGuards [ i ] . key === key ) { this . _navigationGuards . splice ( i , 1 ) ; break ; } } } } export default Router ;