]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor(router): move to a function-based organization
authorEduardo San Martin Morote <posva13@gmail.com>
Sun, 17 Nov 2019 17:49:05 +0000 (18:49 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Sun, 17 Nov 2019 17:49:05 +0000 (18:49 +0100)
14 files changed:
__tests__/errors.spec.ts
__tests__/guards/component-beforeRouteEnter.spec.ts
__tests__/guards/component-beforeRouteLeave.spec.ts
__tests__/guards/component-beforeRouteUpdate.spec.ts
__tests__/guards/global-after.spec.ts
__tests__/guards/global-beforeEach.spec.ts
__tests__/guards/route-beforeEnter.spec.ts
__tests__/router.spec.ts
__tests__/ssr/shared.ts
__tests__/url-encoding.spec.ts
explorations/html5.ts
src/index.ts
src/matcher/index.ts
src/router.ts

index 61b94575dd97a863df9bca13cf891da48ca15b4d..5c48509fd2087109cb4ff3057d5f47d1e50e5f31 100644 (file)
@@ -1,4 +1,4 @@
-import { Router, createMemoryHistory } from '../src'
+import { createRouter as newRouter, createMemoryHistory } from '../src'
 import { NavigationAborted, NavigationGuardRedirect } from '../src/errors'
 import { components, tick } from './utils'
 import { RouteRecord } from '../src/types'
@@ -24,7 +24,7 @@ const routes: RouteRecord[] = [
 const onError = jest.fn()
 function createRouter() {
   const history = createMemoryHistory()
-  const router = new Router({
+  const router = newRouter({
     history,
     routes,
   })
index 3c4ecda4e4afcb4c84cf98f6cb7700236c60374d..98e07f276609a92d42036211567e4b2c7be038d4 100644 (file)
@@ -1,13 +1,13 @@
-import { RouterOptions } from '../../src/router'
+import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { NAVIGATION_TYPES, createDom, noGuard } from '../utils'
 import { RouteRecord, NavigationGuard } from '../../src/types'
-import { Router, createHistory } from '../../src'
+import { createHistory } from '../../src'
 
 function createRouter(
   options: Partial<RouterOptions> & { routes: RouteRecord[] }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index 204ddf4df7406a2c4824815dea8f9263b8eddf3d..3b0479caeab67240ad12751c543be8b584fba2a6 100644 (file)
@@ -1,4 +1,4 @@
-import { RouterOptions, Router } from '../../src/router'
+import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import { NAVIGATION_TYPES, createDom, noGuard } from '../utils'
 import { RouteRecord } from '../../src/types'
 import { createHistory } from '../../src'
@@ -7,7 +7,7 @@ import { createHistory } from '../../src'
 function createRouter(
   options: Partial<RouterOptions> & { routes: RouteRecord[] }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index 63625ca247ffcc5987b4a230347748de98a3c59e..7d4fedfee0d757745f48c8b5fdd04073aa7ba257 100644 (file)
@@ -1,6 +1,6 @@
 import fakePromise from 'faked-promise'
 import { NAVIGATION_TYPES, createDom, noGuard } from '../utils'
-import { Router, createHistory } from '../../src'
+import { createRouter as newRouter, createHistory } from '../../src'
 import { RouteRecord } from '../../src/types'
 
 function createRouter(
@@ -8,7 +8,7 @@ function createRouter(
     routes: import('../../src/types').RouteRecord[]
   }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index 5cd56e5912e86471e21ced793561bb283b159116..e98dde1d9e13bdc66d316ae4279b905fcd347199 100644 (file)
@@ -1,12 +1,12 @@
 import { NAVIGATION_TYPES, createDom } from '../utils'
-import { createHistory, Router } from '../../src'
+import { createHistory, createRouter as newRouter } from '../../src'
 
 function createRouter(
   options: Partial<import('../../src/router').RouterOptions> & {
     routes: import('../../src/types').RouteRecord[]
   }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index eb35ae3ff79f469d2b222d1dde1ac47ea185858a..bc6df68ffe54eb2639c5db366d9f29a696c2d9aa 100644 (file)
@@ -2,12 +2,12 @@ import { RouterOptions } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { NAVIGATION_TYPES, createDom, tick, noGuard } from '../utils'
 import { RouteRecord, RouteLocation } from '../../src/types'
-import { createHistory, Router } from '../../src'
+import { createHistory, createRouter as newRouter } from '../../src'
 
 function createRouter(
   options: Partial<RouterOptions> & { routes: RouteRecord[] }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index 51e908ced1147e4e761235cb522399b962e3702f..8f98ee1be97e4c85a9080ef5273f615f546d8d22 100644 (file)
@@ -1,4 +1,4 @@
-import { RouterOptions, Router } from '../../src/router'
+import { RouterOptions, createRouter as newRouter } from '../../src/router'
 import fakePromise from 'faked-promise'
 import { NAVIGATION_TYPES, createDom, noGuard, tick } from '../utils'
 import { RouteRecord } from '../../src/types'
@@ -7,7 +7,7 @@ import { createHistory } from '../../src'
 function createRouter(
   options: Partial<RouterOptions> & { routes: RouteRecord[] }
 ) {
-  return new Router({
+  return newRouter({
     history: createHistory(),
     ...options,
   })
index 35d1c0ad3a4665d1ce1c05d3c4fa2d36327ff6dc..489b9e488e98cfe50d9b99adffbe9092ebad09dd 100644 (file)
@@ -1,5 +1,5 @@
 import fakePromise from 'faked-promise'
-import { Router, createMemoryHistory, createHistory } from '../src'
+import { createRouter, createMemoryHistory, createHistory } from '../src'
 import { NavigationCancelled } from '../src/errors'
 import { createDom, components, tick } from './utils'
 import { RouteRecord, RouteLocation } from '../src/types'
@@ -23,10 +23,6 @@ const routes: RouteRecord[] = [
   },
 ]
 
-function createRouter(...options: ConstructorParameters<typeof Router>) {
-  return new Router(...options)
-}
-
 describe('Router', () => {
   beforeAll(() => {
     createDom()
index d0d80b83272bad3009be1afac63fe56eafcf4f28..9392cc5658c05e414292ac847530fa4c60f9ef32 100644 (file)
@@ -1,5 +1,9 @@
 import Vue, { ComponentOptions } from 'vue'
-import { Router, createMemoryHistory, plugin } from '../../src'
+import {
+  createRouter as newRouter,
+  createMemoryHistory,
+  plugin,
+} from '../../src'
 import { components } from '../utils'
 
 import { createRenderer } from 'vue-server-renderer'
@@ -11,7 +15,7 @@ export const renderer = createRenderer()
 
 export function createRouter(options?: Partial<RouterOptions>) {
   // TODO: a more complex routing that can be used for most tests
-  return new Router({
+  return newRouter({
     history: createMemoryHistory(),
     routes: [
       {
@@ -56,11 +60,6 @@ export function renderApp(
   return new Promise<ReturnType<typeof createApp>['app']>((resolve, reject) => {
     const { app, router } = createApp(routerOptions, vueOptions)
 
-    // set server-side router's location
-    router.push(context.url).catch(err => {
-      console.error('ssr push failed', err)
-    })
-
     // wait until router has resolved possible async components and hooks
     // TODO: rename the promise one to isReady
     router.onReady().then(() => {
@@ -74,5 +73,10 @@ export function renderApp(
       // the Promise should resolve to the app instance so it can be rendered
       resolve(app)
     }, reject)
+
+    // set server-side router's location
+    router.push(context.url).catch(err => {
+      console.error('ssr push failed', err)
+    })
   })
 }
index ca39a2a8d10dc10aa2cf8dbfd08d5141db7507f7..c770db503d5780211e019821be8e27c0844841ce 100644 (file)
@@ -1,4 +1,4 @@
-import { Router } from '../src/router'
+import { createRouter } from '../src/router'
 import { createDom, components } from './utils'
 import { RouteRecord } from '../src/types'
 import { createMemoryHistory } from '../src'
@@ -25,7 +25,7 @@ describe('URL Encoding', () => {
   describe('initial navigation', () => {
     it('decodes path', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.replace('/%25')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -39,7 +39,7 @@ describe('URL Encoding', () => {
     it('decodes params in path', async () => {
       // /p/€
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/p/%E2%82%AC')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -53,7 +53,7 @@ describe('URL Encoding', () => {
 
     it('allows navigating to valid unencoded params (IE and Edge)', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/p/€')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -70,7 +70,7 @@ describe('URL Encoding', () => {
     it('allows navigating to invalid unencoded params (IE and Edge)', async () => {
       const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/p/%notvalid')
       expect(spy).toHaveBeenCalledTimes(1)
       spy.mockRestore()
@@ -88,7 +88,7 @@ describe('URL Encoding', () => {
 
     it('decodes params in query', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/?q=%25%E2%82%AC')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -104,7 +104,7 @@ describe('URL Encoding', () => {
 
     it('decodes params keys in query', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/?%E2%82%AC=euro')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -121,7 +121,7 @@ describe('URL Encoding', () => {
     it('allow unencoded params in query (IE Edge)', async () => {
       const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/?q=€%notvalid')
       expect(spy).toHaveBeenCalledTimes(1)
       spy.mockRestore()
@@ -142,7 +142,7 @@ describe('URL Encoding', () => {
     // encoding it. To be safe we would have to encode everything
     it.skip('decodes hash', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/#%25%E2%82%AC')
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
@@ -157,7 +157,7 @@ describe('URL Encoding', () => {
     it('allow unencoded params in query (IE Edge)', async () => {
       const spy = jest.spyOn(console, 'warn').mockImplementation(() => {})
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push('/?q=€%notvalid')
       expect(spy).toHaveBeenCalledTimes(1)
       spy.mockRestore()
@@ -177,7 +177,7 @@ describe('URL Encoding', () => {
   describe('resolving locations', () => {
     it('encodes params when resolving', async () => {
       const history = createHistory()
-      const router = new Router({ history, routes })
+      const router = createRouter({ history, routes })
       await router.push({ name: 'params', params: { p: '%€' } })
       expect(router.currentRoute).toEqual(
         expect.objectContaining({
index 086ec8aa8da8343e5720902e63cbcf85e8aae5f5..bb1adad4e51cfce58739c775fed863e538bf451b 100644 (file)
@@ -1,5 +1,5 @@
 import {
-  Router,
+  createRouter,
   plugin,
   // @ts-ignore
   createHistory,
@@ -16,7 +16,7 @@ declare global {
     vm: Vue
     // h: HTML5History
     h: ReturnType<typeof createHistory>
-    r: Router
+    r: ReturnType<typeof createRouter>
   }
 }
 
@@ -33,6 +33,10 @@ const component: RouteComponent = {
   template: `<div>A component</div>`,
 }
 
+const NotFound: RouteComponent = {
+  template: `<div>Not Found: {{ $route.fullPath }}</div>`,
+}
+
 const Home: RouteComponent = {
   template: `<div>Home</div>`,
 }
@@ -110,7 +114,7 @@ const scrollWaiter = new ScrollQueue()
 
 // const hist = new HTML5History()
 // const hist = new HashHistory()
-const router = new Router({
+const router = createRouter({
   history: routerHistory,
   routes: [
     { path: '/', component: Home, name: 'home', alias: '/home' },
@@ -141,7 +145,7 @@ const router = new Router({
     },
     { path: '/with-data', component: ComponentWithData, name: 'WithData' },
     { path: '/rep/:a*', component: component, name: 'repeat' },
-    // { path: /^\/about\/?$/, component },
+    { path: '/:data(.*)', component: NotFound, name: 'NotFound' },
   ],
   async scrollBehavior(to, from, savedPosition) {
     await scrollWaiter.wait()
index 1046823606405b855b5348adc4653329a1031006..3af176418015cf1cedb2bf05c34967b11fb9a8cb 100644 (file)
@@ -1,5 +1,5 @@
-import { Router, RouterOptions } from './router'
-import { PluginFunction, VueConstructor } from 'vue'
+import { createRouter, Router } from './router'
+import { PluginFunction } from 'vue'
 import createHistory from './history/html5'
 import createMemoryHistory from './history/memory'
 import createHashHistory from './history/hash'
@@ -19,8 +19,7 @@ const plugin: PluginFunction<void> = Vue => {
         // @ts-ignore _router is internal
         this._router = router
         // this._router.init(this)
-        // @ts-ignore
-        this._router.app = this
+        router.setActiveApp(this)
         // @ts-ignore we can use but should not be used by others
         Vue.util.defineReactive(
           this,
@@ -64,7 +63,13 @@ const plugin: PluginFunction<void> = Vue => {
     strats.created
 }
 
-export { Router, createHistory, createMemoryHistory, createHashHistory, plugin }
+export {
+  createRouter,
+  createHistory,
+  createMemoryHistory,
+  createHashHistory,
+  plugin,
+}
 
 // TODO: refactor somewhere else
 // const inBrowser = typeof window !== 'undefined'
@@ -75,30 +80,30 @@ export { Router, createHistory, createMemoryHistory, createHashHistory, plugin }
 //   abstract: AbstractHistory
 // }
 
-export default class VueRouter extends Router {
-  static install = plugin
-  static version = '__VERSION__'
+// export default class VueRouter extends Router {
+//   static install = plugin
+//   static version = '__VERSION__'
 
-  // TODO: handle mode in a retro compatible way
-  constructor(
-    options: Partial<RouterOptions & { mode: 'history' | 'abstract' | 'hash' }>
-  ) {
-    // let { mode } = options
-    // if (!inBrowser) mode = 'abstract'
-    super({
-      ...options,
-      routes: options.routes || [],
-      // FIXME: change when possible
-      history: createHistory(),
-      // history: new HistoryMode[mode || 'hash'](),
-    })
-  }
-}
+//   // TODO: handle mode in a retro compatible way
+//   constructor(
+//     options: Partial<RouterOptions & { mode: 'history' | 'abstract' | 'hash' }>
+//   ) {
+//     // let { mode } = options
+//     // if (!inBrowser) mode = 'abstract'
+//     super({
+//       ...options,
+//       routes: options.routes || [],
+//       // FIXME: change when possible
+//       history: createHistory(),
+//       // history: new HistoryMode[mode || 'hash'](),
+//     })
+//   }
+// }
 
-declare global {
-  interface Window {
-    Vue: VueConstructor
-  }
-}
+// declare global {
+//   interface Window {
+//     Vue: VueConstructor
+//   }
+// }
 
-if (typeof window !== 'undefined' && window.Vue) window.Vue.use(VueRouter)
+// if (typeof window !== 'undefined' && window.Vue) window.Vue.use(VueRouter)
index cf59f4f0c55e24c6f7e5bbe02c2df19a811b8115..d2c8a74a0a6bbd5c28aaa7203fede62744ec5810 100644 (file)
@@ -14,6 +14,7 @@ import { RouteRecordMatcher, RouteRecordNormalized } from './types'
 
 interface RouterMatcher {
   addRoute: (record: Readonly<RouteRecord>, parent?: RouteRecordMatcher) => void
+  // TODO: remove route
   resolve: (
     location: Readonly<MatcherLocation>,
     currentLocation: Readonly<MatcherLocationNormalized>
index 72beb9818a7169fb0d6ddedaefa168bcc90a771d..26366196ea8f1dd5b877c1f821597297dc8c5afc 100644 (file)
@@ -1,37 +1,41 @@
 import {
-  normalizeLocation,
+  RouteLocationNormalized,
+  RouteRecord,
+  RouteLocation,
+  NavigationGuard,
+  ListenerRemover,
+  PostNavigationGuard,
+  START_LOCATION_NORMALIZED,
+  MatcherLocation,
+  RouteQueryAndHash,
+  Lazy,
+  TODO,
+} from './types'
+import {
   RouterHistory,
+  normalizeLocation,
   stringifyURL,
   normalizeQuery,
   HistoryLocationNormalized,
   START,
 } from './history/common'
-import { createRouterMatcher } from './matcher'
-import {
-  RouteLocation,
-  RouteRecord,
-  START_LOCATION_NORMALIZED,
-  RouteLocationNormalized,
-  ListenerRemover,
-  NavigationGuard,
-  TODO,
-  PostNavigationGuard,
-  Lazy,
-  MatcherLocation,
-  RouteQueryAndHash,
-} from './types/index'
 import {
   ScrollToPosition,
   ScrollPosition,
   scrollToPosition,
 } from './utils/scroll'
-
-import { guardToPromiseFn, extractComponentsGuards } from './utils'
+import { createRouterMatcher } from './matcher'
 import {
+  NavigationCancelled,
   NavigationGuardRedirect,
   NavigationAborted,
-  NavigationCancelled,
 } from './errors'
+import { extractComponentsGuards, guardToPromiseFn } from './utils'
+import Vue from 'vue'
+
+type ErrorHandler = (error: any) => any
+// resolve, reject arguments of Promise constructor
+type OnReadyCallback = [() => void, (reason?: any) => void]
 
 interface ScrollBehavior {
   (
@@ -47,129 +51,82 @@ export interface RouterOptions {
   scrollBehavior?: ScrollBehavior
 }
 
-type ErrorHandler = (error: any) => any
-
-// resolve, reject arguments of Promise constructor
-type OnReadyCallback = [() => void, (reason?: any) => void]
-export class Router {
-  protected history: RouterHistory
-  private matcher: ReturnType<typeof createRouterMatcher>
-  private beforeGuards: NavigationGuard[] = []
-  private afterGuards: PostNavigationGuard[] = []
-  currentRoute: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
-  pendingLocation: Readonly<RouteLocationNormalized> = START_LOCATION_NORMALIZED
-  private app: any
-  // TODO: should these be triggered before or after route.push().catch()
-  private errorHandlers: ErrorHandler[] = []
-  private ready: boolean = false
-  private onReadyCbs: OnReadyCallback[] = []
-  private scrollBehavior?: ScrollBehavior
-
-  constructor(options: RouterOptions) {
-    this.history = options.history
-    // this.history.ensureLocation()
-    this.scrollBehavior = options.scrollBehavior
+export interface Router {
+  currentRoute: Readonly<RouteLocationNormalized>
 
-    this.matcher = createRouterMatcher(options.routes)
+  resolve(to: RouteLocation): RouteLocationNormalized
+  createHref(to: RouteLocationNormalized): string
+  push(to: RouteLocation): Promise<RouteLocationNormalized>
+  replace(to: RouteLocation): Promise<RouteLocationNormalized>
 
-    this.history.listen(async (to, from, info) => {
-      const matchedRoute = this.resolveLocation(to, this.currentRoute)
-      // console.log({ to, matchedRoute })
+  // TODO: find a way to remove it
+  doInitialNavigation(): Promise<void>
+  setActiveApp(vm: Vue): void
 
-      const toLocation: RouteLocationNormalized = { ...to, ...matchedRoute }
-      this.pendingLocation = toLocation
+  beforeEach(guard: NavigationGuard): ListenerRemover
+  afterEach(guard: PostNavigationGuard): ListenerRemover
 
-      try {
-        await this.navigate(toLocation, this.currentRoute)
-
-        // a more recent navigation took place
-        if (this.pendingLocation !== toLocation) {
-          return this.triggerError(
-            new NavigationCancelled(toLocation, this.currentRoute),
-            false
-          )
-        }
+  // TODO: also return a ListenerRemover
+  onError(handler: ErrorHandler): void
+  // TODO: change to isReady
+  onReady(): Promise<void>
+}
 
-        // accept current navigation
-        this.currentRoute = {
-          ...to,
-          ...matchedRoute,
-        }
-        this.updateReactiveRoute()
-        // TODO: refactor with a state getter
-        // const { scroll } = this.history.state
-        const { state } = window.history
-        this.handleScroll(toLocation, this.currentRoute, state.scroll).catch(
-          err => this.triggerError(err, false)
-        )
-      } catch (error) {
-        if (NavigationGuardRedirect.is(error)) {
-          // TODO: refactor the duplication of new NavigationCancelled by
-          // checking instanceof NavigationError (it's another TODO)
-          // a more recent navigation took place
-          if (this.pendingLocation !== toLocation) {
-            return this.triggerError(
-              new NavigationCancelled(toLocation, this.currentRoute),
-              false
-            )
-          }
-          this.triggerError(error, false)
-
-          // the error is already handled by router.push
-          // we just want to avoid logging the error
-          this.push(error.to).catch(() => {})
-        } else if (NavigationAborted.is(error)) {
-          console.log('Cancelled, going to', -info.distance)
-          this.history.go(-info.distance, false)
-          // TODO: test on different browsers ensure consistent behavior
-          // Maybe we could write the length the first time we do a navigation and use that for direction
-          // TODO: this doesn't work if the user directly calls window.history.go(-n) with n > 1
-          // We can override the go method to retrieve the number but not sure if all browsers allow that
-          // if (info.direction === NavigationDirection.back) {
-          //   this.history.forward(false)
-          // } else {
-          // TODO: go back because we cancelled, then
-          // or replace and not discard the rest of history. Check issues, there was one talking about this
-          // behaviour, maybe we can do better
-          // this.history.back(false)
-          // }
-        } else {
-          this.triggerError(error, false)
-        }
-      }
-    })
-  }
+export function createRouter({
+  history,
+  routes,
+  scrollBehavior,
+}: RouterOptions): Router {
+  const matcher: ReturnType<typeof createRouterMatcher> = createRouterMatcher(
+    routes
+  )
+  const beforeGuards: NavigationGuard[] = []
+  const afterGuards: PostNavigationGuard[] = []
+  let currentRoute: Readonly<
+    RouteLocationNormalized
+  > = START_LOCATION_NORMALIZED
+  let pendingLocation: Readonly<
+    RouteLocationNormalized
+  > = START_LOCATION_NORMALIZED
+  let onReadyCbs: OnReadyCallback[] = []
+  // TODO: should these be triggered before or after route.push().catch()
+  let errorHandlers: ErrorHandler[] = []
+  let app: Vue
+  let ready: boolean = false
 
-  resolve(
+  function resolve(
     to: RouteLocation,
     currentLocation?: RouteLocationNormalized /*, append?: boolean */
   ): RouteLocationNormalized {
     if (typeof to === 'string')
-      return this.resolveLocation(
+      return resolveLocation(
         // TODO: refactor and remove import
         normalizeLocation(to),
         currentLocation
       )
-    return this.resolveLocation({
-      // TODO: refactor with url utils
-      query: {},
-      hash: '',
-      ...to,
-    })
+    return resolveLocation(
+      {
+        // TODO: refactor with url utils
+        query: {},
+        hash: '',
+        ...to,
+      },
+      currentLocation
+    )
   }
 
-  createHref(to: RouteLocationNormalized): string {
-    return this.history.base + to.fullPath
+  function createHref(to: RouteLocationNormalized): string {
+    return history.base + to.fullPath
   }
 
-  private resolveLocation(
+  function resolveLocation(
     location: MatcherLocation & Required<RouteQueryAndHash>,
     currentLocation?: RouteLocationNormalized,
     redirectedFrom?: RouteLocationNormalized
     // ensure when returning that the redirectedFrom is a normalized location
   ): RouteLocationNormalized {
-    currentLocation = currentLocation || this.currentRoute
-    const matchedRoute = this.matcher.resolve(location, currentLocation)
+    currentLocation = currentLocation || currentRoute
+    const matchedRoute = matcher.resolve(location, currentLocation)
 
     if ('redirect' in matchedRoute) {
       const { redirect } = matchedRoute
@@ -189,7 +146,7 @@ export class Router {
 
       if (typeof redirect === 'string') {
         // match the redirect instead
-        return this.resolveLocation(
+        return resolveLocation(
           normalizeLocation(redirect),
           currentLocation,
           normalizedLocation
@@ -198,7 +155,7 @@ export class Router {
         const newLocation = redirect(normalizedLocation)
 
         if (typeof newLocation === 'string') {
-          return this.resolveLocation(
+          return resolveLocation(
             normalizeLocation(newLocation),
             currentLocation,
             normalizedLocation
@@ -209,7 +166,7 @@ export class Router {
         // there was a redirect before
         // if (!('path' in newLocation) && !('name' in newLocation)) throw new Error('TODO: redirect canot be relative')
 
-        return this.resolveLocation(
+        return resolveLocation(
           {
             ...newLocation,
             query: normalizeQuery(newLocation.query || {}),
@@ -219,7 +176,7 @@ export class Router {
           normalizedLocation
         )
       } else {
-        return this.resolveLocation(
+        return resolveLocation(
           {
             ...redirect,
             query: normalizeQuery(redirect.query || {}),
@@ -244,44 +201,20 @@ export class Router {
     }
   }
 
-  /**
-   * Get an array of matched components for a location. TODO: check if the array should contain plain components
-   * instead of functions that return promises for lazy loaded components
-   * @param to location to geth matched components from. If not provided, uses current location instead
-   */
-  // getMatchedComponents(
-  //   to?: RouteLocation | RouteLocationNormalized
-  // ): RouteComponent[] {
-  //   const location = to
-  //     ? typeof to !== 'string' && 'matched' in to
-  //       ? to
-  //       : this.resolveLocation(typeof to === 'string' ? this.history.utils.normalizeLocation(to) : to)
-  //     : this.currentRoute
-  //   if (!location) return []
-  //   return location.matched.map(m =>
-  //     Object.keys(m.components).map(name => m.components[name])
-  //   )
-  // }
-
-  /**
-   * Trigger a navigation, adding an entry to the history stack. Also apply all navigation
-   * guards first
-   * @param to where to go
-   */
-  async push(to: RouteLocation): Promise<RouteLocationNormalized> {
+  async function push(to: RouteLocation): Promise<RouteLocationNormalized> {
     let url: HistoryLocationNormalized
     let location: RouteLocationNormalized
     // TODO: refactor into matchLocation to return location and url
     if (typeof to === 'string' || ('path' in to && !('name' in to))) {
       url = normalizeLocation(to)
       // TODO: should allow a non matching url to allow dynamic routing to work
-      location = this.resolveLocation(url, this.currentRoute)
+      location = resolveLocation(url, currentRoute)
     } else {
       // named or relative route
       const query = to.query ? normalizeQuery(to.query) : {}
       const hash = to.hash || ''
       // we need to resolve first
-      location = this.resolveLocation({ ...to, query, hash }, this.currentRoute)
+      location = resolveLocation({ ...to, query, hash }, currentRoute)
       // intentionally drop current query and hash
       url = normalizeLocation({
         query,
@@ -293,82 +226,70 @@ export class Router {
     // TODO: should we throw an error as the navigation was aborted
     // TODO: needs a proper check because order in query could be different
     if (
-      this.currentRoute !== START_LOCATION_NORMALIZED &&
-      this.currentRoute.fullPath === url.fullPath
+      currentRoute !== START_LOCATION_NORMALIZED &&
+      currentRoute.fullPath === url.fullPath
     )
-      return this.currentRoute
+      return currentRoute
 
     const toLocation: RouteLocationNormalized = location
-    this.pendingLocation = toLocation
+    pendingLocation = toLocation
     // trigger all guards, throw if navigation is rejected
     try {
-      await this.navigate(toLocation, this.currentRoute)
+      await navigate(toLocation, currentRoute)
     } catch (error) {
       if (NavigationGuardRedirect.is(error)) {
         // push was called while waiting in guards
-        if (this.pendingLocation !== toLocation) {
+        if (pendingLocation !== toLocation) {
           // TODO: trigger onError as well
-          throw new NavigationCancelled(toLocation, this.currentRoute)
+          throw new NavigationCancelled(toLocation, currentRoute)
         }
         // TODO: setup redirect stack
         // TODO: shouldn't we trigger the error as well
-        return this.push(error.to)
+        return push(error.to)
       } else {
         // TODO: write tests
         // triggerError as well
-        if (this.pendingLocation !== toLocation) {
+        if (pendingLocation !== toLocation) {
           // TODO: trigger onError as well
-          throw new NavigationCancelled(toLocation, this.currentRoute)
+          throw new NavigationCancelled(toLocation, currentRoute)
         }
 
-        this.triggerError(error)
+        triggerError(error)
       }
     }
 
     // push was called while waiting in guards
-    if (this.pendingLocation !== toLocation) {
-      throw new NavigationCancelled(toLocation, this.currentRoute)
+    if (pendingLocation !== toLocation) {
+      throw new NavigationCancelled(toLocation, currentRoute)
     }
 
     // change URL
-    if (to.replace === true) this.history.replace(url)
-    else this.history.push(url)
-
-    const from = this.currentRoute
-    this.currentRoute = toLocation
-    this.updateReactiveRoute()
-    this.handleScroll(toLocation, from).catch(err =>
-      this.triggerError(err, false)
-    )
+    if (to.replace === true) history.replace(url)
+    else history.push(url)
+
+    const from = currentRoute
+    currentRoute = toLocation
+    updateReactiveRoute()
+    handleScroll(toLocation, from).catch(err => triggerError(err, false))
 
     // navigation is confirmed, call afterGuards
-    for (const guard of this.afterGuards) guard(toLocation, from)
+    for (const guard of afterGuards) guard(toLocation, from)
 
-    return this.currentRoute
+    return currentRoute
   }
 
-  /**
-   * Trigger a navigation, replacing current entry in history. Also apply all navigation
-   * guards first
-   * @param to where to go
-   */
-  replace(to: RouteLocation) {
+  function replace(to: RouteLocation) {
     const location = typeof to === 'string' ? { path: to } : to
-    return this.push({ ...location, replace: true })
+    return push({ ...location, replace: true })
   }
 
-  /**
-   * Runs a guard queue and handles redirects, rejections
-   * @param guards Array of guards converted to functions that return a promise
-   * @returns {boolean} true if the navigation should be cancelled false otherwise
-   */
-  private async runGuardQueue(guards: Lazy<any>[]): Promise<void> {
+  async function runGuardQueue(guards: Lazy<any>[]): Promise<void> {
     for (const guard of guards) {
       await guard()
     }
   }
 
-  private async navigate(
+  async function navigate(
     to: RouteLocationNormalized,
     from: RouteLocationNormalized
   ): Promise<TODO> {
@@ -383,16 +304,16 @@ export class Router {
     )
 
     // run the queue of per route beforeRouteLeave guards
-    await this.runGuardQueue(guards)
+    await runGuardQueue(guards)
 
     // check global guards beforeEach
     guards = []
-    for (const guard of this.beforeGuards) {
+    for (const guard of beforeGuards) {
       guards.push(guardToPromiseFn(guard, to, from))
     }
 
     // console.log('Guarding against', guards.length, 'guards')
-    await this.runGuardQueue(guards)
+    await runGuardQueue(guards)
 
     // check in components beforeRouteUpdate
     guards = await extractComponentsGuards(
@@ -403,7 +324,7 @@ export class Router {
     )
 
     // run the queue of per route beforeEnter guards
-    await this.runGuardQueue(guards)
+    await runGuardQueue(guards)
 
     // check the route beforeEnter
     guards = []
@@ -420,7 +341,7 @@ export class Router {
     }
 
     // run the queue of per route beforeEnter guards
-    await this.runGuardQueue(guards)
+    await runGuardQueue(guards)
 
     // check in-component beforeRouteEnter
     // TODO: is it okay to resolve all matched component or should we do it in order
@@ -432,163 +353,229 @@ export class Router {
     )
 
     // run the queue of per route beforeEnter guards
-    await this.runGuardQueue(guards)
+    await runGuardQueue(guards)
   }
 
-  /**
-   * Add a global beforeGuard that can confirm, abort or modify a navigation
-   * @param guard
-   */
-  beforeEach(guard: NavigationGuard): ListenerRemover {
-    this.beforeGuards.push(guard)
+  history.listen(async (to, from, info) => {
+    const matchedRoute = resolveLocation(to, currentRoute)
+    // console.log({ to, matchedRoute })
+
+    const toLocation: RouteLocationNormalized = { ...to, ...matchedRoute }
+    pendingLocation = toLocation
+
+    try {
+      await navigate(toLocation, currentRoute)
+
+      // a more recent navigation took place
+      if (pendingLocation !== toLocation) {
+        return triggerError(
+          new NavigationCancelled(toLocation, currentRoute),
+          false
+        )
+      }
+
+      // accept current navigation
+      currentRoute = {
+        ...to,
+        ...matchedRoute,
+      }
+      updateReactiveRoute()
+      // TODO: refactor with a state getter
+      // const { scroll } = history.state
+      const { state } = window.history
+      handleScroll(toLocation, currentRoute, state.scroll).catch(err =>
+        triggerError(err, false)
+      )
+    } catch (error) {
+      if (NavigationGuardRedirect.is(error)) {
+        // TODO: refactor the duplication of new NavigationCancelled by
+        // checking instanceof NavigationError (it's another TODO)
+        // a more recent navigation took place
+        if (pendingLocation !== toLocation) {
+          return triggerError(
+            new NavigationCancelled(toLocation, currentRoute),
+            false
+          )
+        }
+        triggerError(error, false)
+
+        // the error is already handled by router.push
+        // we just want to avoid logging the error
+        push(error.to).catch(() => {})
+      } else if (NavigationAborted.is(error)) {
+        console.log('Cancelled, going to', -info.distance)
+        history.go(-info.distance, false)
+        // TODO: test on different browsers ensure consistent behavior
+        // Maybe we could write the length the first time we do a navigation and use that for direction
+        // TODO: this doesn't work if the user directly calls window.history.go(-n) with n > 1
+        // We can override the go method to retrieve the number but not sure if all browsers allow that
+        // if (info.direction === NavigationDirection.back) {
+        //   history.forward(false)
+        // } else {
+        // TODO: go back because we cancelled, then
+        // or replace and not discard the rest of history. Check issues, there was one talking about this
+        // behaviour, maybe we can do better
+        // history.back(false)
+        // }
+      } else {
+        triggerError(error, false)
+      }
+    }
+  })
+
+  function beforeEach(guard: NavigationGuard): ListenerRemover {
+    beforeGuards.push(guard)
     return () => {
-      const i = this.beforeGuards.indexOf(guard)
-      if (i > -1) this.beforeGuards.splice(i, 1)
+      const i = beforeGuards.indexOf(guard)
+      if (i > -1) beforeGuards.splice(i, 1)
     }
   }
 
-  /**
-   * Add a global after guard that is called once the navigation is confirmed
-   * @param guard
-   */
-  afterEach(guard: PostNavigationGuard): ListenerRemover {
-    this.afterGuards.push(guard)
+  function afterEach(guard: PostNavigationGuard): ListenerRemover {
+    afterGuards.push(guard)
     return () => {
-      const i = this.afterGuards.indexOf(guard)
-      if (i > -1) this.afterGuards.splice(i, 1)
+      const i = afterGuards.indexOf(guard)
+      if (i > -1) afterGuards.splice(i, 1)
     }
   }
 
-  /**
-   * Add an error handler to catch errors during navigation
-   * TODO: return a remover like beforeEach
-   * @param handler error handler
-   */
-  onError(handler: ErrorHandler): void {
-    this.errorHandlers.push(handler)
+  function onError(handler: ErrorHandler): void {
+    errorHandlers.push(handler)
   }
 
-  /**
-   * Trigger all registered error handlers
-   * @param error thrown error
-   * @param shouldThrow set to false to not throw the error
-   */
-  private triggerError(error: any, shouldThrow: boolean = true): void {
-    for (const handler of this.errorHandlers) {
+  function triggerError(error: any, shouldThrow: boolean = true): void {
+    for (const handler of errorHandlers) {
       handler(error)
     }
     if (shouldThrow) throw error
   }
 
-  private updateReactiveRoute() {
-    if (!this.app) return
+  function updateReactiveRoute() {
+    if (!app) return
     // TODO: matched should be non enumerable and the defineProperty here shouldn't be necessary
-    const route = { ...this.currentRoute }
+    const route = { ...currentRoute }
     Object.defineProperty(route, 'matched', { enumerable: false })
-    this.app._route = Object.freeze(route)
-    this.markAsReady()
+    // @ts-ignore
+    app._route = Object.freeze(route)
+    markAsReady()
   }
 
-  /**
-   * Returns a Promise that resolves once the router is ready to be used for navigation
-   * Eg: Calling router.push() or router.replace(). This is necessary because we have to
-   * wait for the Vue root instance to be created
-   */
-  onReady(): Promise<void> {
-    if (this.ready) return Promise.resolve()
+  function onReady(): Promise<void> {
+    if (ready && currentRoute !== START_LOCATION_NORMALIZED)
+      return Promise.resolve()
     return new Promise((resolve, reject) => {
-      this.onReadyCbs.push([resolve, reject])
+      onReadyCbs.push([resolve, reject])
     })
   }
 
-  /**
-   * Mark the router as ready. This function is used internally and should not be called
-   * by the developper. You can optionally provide an error.
-   * This will trigger all onReady callbacks and empty the array
-   * @param err optional error if navigation failed
-   */
-  protected markAsReady(err?: any): void {
-    if (this.ready) return
-    for (const [resolve] of this.onReadyCbs) {
+  function markAsReady(err?: any): void {
+    if (ready || currentRoute === START_LOCATION_NORMALIZED) return
+    ready = true
+    for (const [resolve] of onReadyCbs) {
       // TODO: is this okay?
       // always resolve, as the router is ready even if there was an error
       // @ts-ignore
       resolve(err)
+      // TODO: try catch the on ready?
       // if (err) reject(err)
       // else resolve()
     }
-    this.onReadyCbs = []
-    this.ready = true
+    onReadyCbs = []
   }
 
-  // TODO: rename to ensureInitialLocation
-  async doInitialNavigation(): Promise<void> {
+  async function doInitialNavigation(): Promise<void> {
     // let the user call replace or push on SSR
-    if (this.history.location === START) return
+    if (history.location === START) return
     // TODO: refactor code that was duplicated from push method
-    const toLocation: RouteLocationNormalized = this.resolveLocation(
-      this.history.location,
-      this.currentRoute
+    const toLocation: RouteLocationNormalized = resolveLocation(
+      history.location,
+      currentRoute
     )
 
-    this.pendingLocation = toLocation
+    pendingLocation = toLocation
     // trigger all guards, throw if navigation is rejected
     try {
-      await this.navigate(toLocation, this.currentRoute)
+      await navigate(toLocation, currentRoute)
     } catch (error) {
-      this.markAsReady(error)
+      markAsReady(error)
       if (NavigationGuardRedirect.is(error)) {
         // push was called while waiting in guards
-        if (this.pendingLocation !== toLocation) {
+        if (pendingLocation !== toLocation) {
           // TODO: trigger onError as well
-          throw new NavigationCancelled(toLocation, this.currentRoute)
+          throw new NavigationCancelled(toLocation, currentRoute)
         }
         // TODO: setup redirect stack
-        await this.push(error.to)
+        await push(error.to)
         return
       } else {
         // TODO: write tests
         // triggerError as well
-        if (this.pendingLocation !== toLocation) {
+        if (pendingLocation !== toLocation) {
           // TODO: trigger onError as well
-          throw new NavigationCancelled(toLocation, this.currentRoute)
+          throw new NavigationCancelled(toLocation, currentRoute)
         }
 
         // this throws, so nothing ahead happens
-        this.triggerError(error)
+        triggerError(error)
       }
     }
 
     // push was called while waiting in guards
-    if (this.pendingLocation !== toLocation) {
-      const error = new NavigationCancelled(toLocation, this.currentRoute)
-      this.markAsReady(error)
+    if (pendingLocation !== toLocation) {
+      const error = new NavigationCancelled(toLocation, currentRoute)
+      markAsReady(error)
       throw error
     }
 
     // NOTE: here we removed the pushing to history part as the history
     // already contains current location
 
-    const from = this.currentRoute
-    this.currentRoute = toLocation
-    this.updateReactiveRoute()
+    const from = currentRoute
+    currentRoute = toLocation
+    updateReactiveRoute()
 
     // navigation is confirmed, call afterGuards
-    for (const guard of this.afterGuards) guard(toLocation, from)
+    for (const guard of afterGuards) guard(toLocation, from)
 
-    this.markAsReady()
+    markAsReady()
   }
 
-  private async handleScroll(
+  async function handleScroll(
     to: RouteLocationNormalized,
     from: RouteLocationNormalized,
     scrollPosition?: ScrollToPosition
   ) {
-    if (!this.scrollBehavior) return
+    if (!scrollBehavior) return
 
-    await this.app.$nextTick()
-    const position = await this.scrollBehavior(to, from, scrollPosition || null)
+    await app.$nextTick()
+    const position = await scrollBehavior(to, from, scrollPosition || null)
     console.log('scrolling to', position)
     scrollToPosition(position)
   }
+
+  function setActiveApp(vm: Vue) {
+    app = vm
+    updateReactiveRoute()
+  }
+
+  const router: Router = {
+    currentRoute,
+    push,
+    replace,
+    resolve,
+    beforeEach,
+    afterEach,
+    createHref,
+    onError,
+    onReady,
+
+    doInitialNavigation,
+    setActiveApp,
+  }
+
+  Object.defineProperty(router, 'currentRoute', {
+    get: () => currentRoute,
+  })
+
+  return router
 }