]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor: error with enum types (#123)
author宋铄运 <fnlctrl@gmail.com>
Wed, 18 Mar 2020 16:04:39 +0000 (00:04 +0800)
committerGitHub <noreply@github.com>
Wed, 18 Mar 2020 16:04:39 +0000 (17:04 +0100)
* refactor error with enum codes

* fix lint

* rename error 'code' to 'type'

* refactor

* remove INVALID_ROUTE_MATCH

* fix: lint

* fix: remove ErrorTypeMessages in production build

* fix: types

* fix: add interface NavigationRedirectError

* build: use similar config to vue

* refactor: errors

* build: add banner

* refactor: delete old errors.ts

* refactor: rename errors-new

Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
12 files changed:
__tests__/errors.spec.ts
__tests__/matcher/__snapshots__/resolve.spec.ts.snap
__tests__/router.spec.ts
package.json
rollup.config.js
src/errors.ts
src/global.d.ts
src/index.ts
src/matcher/index.ts
src/router.ts
src/utils/guardToPromiseFn.ts
tsconfig.json

index 5c48509fd2087109cb4ff3057d5f47d1e50e5f31..9ee94107258fa271f4eaedd48b8ff4cd1249f45b 100644 (file)
@@ -1,5 +1,5 @@
 import { createRouter as newRouter, createMemoryHistory } from '../src'
-import { NavigationAborted, NavigationGuardRedirect } from '../src/errors'
+import { ErrorTypes } from '../src/errors'
 import { components, tick } from './utils'
 import { RouteRecord } from '../src/types'
 
@@ -47,9 +47,11 @@ describe('Errors', () => {
     try {
       await router.push('/foo')
     } catch (err) {
-      expect(err).toBeInstanceOf(NavigationAborted)
+      expect(err.type).toBe(ErrorTypes.NAVIGATION_ABORTED)
     }
-    expect(onError).toHaveBeenCalledWith(expect.any(NavigationAborted))
+    expect(onError).toHaveBeenCalledWith(
+      expect.objectContaining({ type: ErrorTypes.NAVIGATION_ABORTED })
+    )
   })
 
   it('triggers erros caused by new navigations of a next(redirect) trigered by history', async () => {
@@ -69,12 +71,15 @@ describe('Errors', () => {
     expect(onError).toHaveBeenCalledTimes(2)
     expect(onError).toHaveBeenNthCalledWith(
       1,
-      expect.any(NavigationGuardRedirect)
+      expect.objectContaining({ type: ErrorTypes.NAVIGATION_GUARD_REDIRECT })
     )
     expect(onError.mock.calls[0]).toMatchObject([
       { to: { params: { p: '1' } }, from: { fullPath: '/p/0' } },
     ])
-    expect(onError).toHaveBeenNthCalledWith(2, expect.any(NavigationAborted))
+    expect(onError).toHaveBeenNthCalledWith(
+      2,
+      expect.objectContaining({ type: ErrorTypes.NAVIGATION_ABORTED })
+    )
     expect(onError.mock.calls[1]).toMatchObject([
       { to: { params: { p: '1' } }, from: { params: { p: 'other' } } },
     ])
index 4592f7d0976e1659e12b16d270710eae8f7fbb02..c9324fbe1dfdd24287f81edd36e1a7556c33e23c 100644 (file)
@@ -1,13 +1,13 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`Router Matcher resolve LocationAsName throws if the named route does not exists 1`] = `
-[NoRouteMatchError: No match for
-{"name":"Home"}]
+[Error: No match for
+ {"name":"Home"}]
 `;
 
 exports[`Router Matcher resolve LocationAsRelative throws if the current named route does not exists 1`] = `
-[NoRouteMatchError: No match for
-{"params":{"a":"foo"}}
+[Error: No match for
+ {"params":{"a":"foo"}}
 while being at
 {"name":"home","params":{},"path":"/","meta":{}}]
 `;
index e6a9a966ff5f6470e212fde8594e4ce78e73ec5b..ea69c117c2fdce1e9dbce4f3b68842aa4b15af99 100644 (file)
@@ -1,6 +1,6 @@
 import fakePromise from 'faked-promise'
 import { createRouter, createMemoryHistory, createWebHistory } from '../src'
-import { NavigationCancelled } from '../src/errors'
+import { ErrorTypes } from '../src/errors'
 import { createDom, components, tick } from './utils'
 import {
   RouteRecord,
@@ -225,7 +225,7 @@ describe('Router', () => {
       try {
         await pA
       } catch (err) {
-        expect(err).toBeInstanceOf(NavigationCancelled)
+        expect(err.type).toBe(ErrorTypes.NAVIGATION_CANCELLED)
       }
       expect(router.currentRoute.value.fullPath).toBe('/p/b')
     }
index 54fbafad295fd7fcea35b996ab96d4290e48810e..4450efe86521451e43f403a70a3e82a377617504 100644 (file)
@@ -3,7 +3,7 @@
   "version": "4.0.0-alpha.3",
   "main": "dist/vue-router.cjs.js",
   "browser": "dist/vue-router.esm.js",
-  "unpkg": "dist/vue-router.js",
+  "unpkg": "dist/vue-router.global.js",
   "module": "dist/vue-router.esm-bundler.js",
   "typings": "dist/vue-router.d.ts",
   "sideEffects": false,
index 5a7d111da01591adb223c0a6b8dc3392017f0d82..c0fb2cc3a92c4eeb129823dc6d65b563fdc072ab 100644 (file)
@@ -1,10 +1,11 @@
+import path from 'path'
+import ts from 'rollup-plugin-typescript2'
 import replace from '@rollup/plugin-replace'
 import resolve from '@rollup/plugin-node-resolve'
 import commonjs from 'rollup-plugin-commonjs'
-import ts from 'rollup-plugin-typescript2'
-import alias from '@rollup/plugin-alias'
-import { terser } from 'rollup-plugin-terser'
-import pkg from './package.json'
+
+const pkg = require('./package.json')
+const name = pkg.name
 
 const banner = `/*!
   * ${pkg.name} v${pkg.version}
@@ -12,108 +13,176 @@ const banner = `/*!
   * @license MIT
   */`
 
-const exportName = 'VueRouter'
-
-function createEntry(
-  {
-    format, // Rollup format (iife, umd, cjs, es)
-    external = ['vue', '@vue/reactivity', '@vue/runtime-core'], // Rollup external option
-    input = 'src/index.ts', // entry point
-    env = 'development', // NODE_ENV variable
-    minify = false,
-    isBrowser = false, // produce a browser module version or not
-  } = {
-    input: 'src/index.ts',
-    env: 'development',
-    minify: false,
-    isBrowser: false,
-  }
-) {
-  // force production mode when minifying
-  if (minify) env = 'production'
-  const isProductionBuild =
-    process.env.__DEV__ === 'false' || env === 'production'
+// ensure TS checks only once for each build
+let hasTSChecked = false
 
-  const config = {
-    input,
-    plugins: [
-      replace({
-        __VERSION__: JSON.stringify(pkg.version),
-        __DEV__:
-          (format === 'es' && !isBrowser) || format === 'cjs'
-            ? // preserve to be handled by bundlers
-              `process.env.NODE_ENV !== 'production'`
-            : // hard coded dev/prod builds
-              !isProductionBuild,
-      }),
-      alias({
-        resolve: ['ts'],
-      }),
-    ],
-    output: {
-      banner,
-      file: 'dist/vue-router.other.js',
-      format,
-      globals: {
-        '@vue/reactivity': 'Vue',
-        '@vue/runtime-core': 'Vue',
-        vue: 'Vue',
-      },
-    },
-  }
+const outputConfigs = {
+  // each file name has the format: `dist/${name}.${format}.js`
+  // format being a key of this object
+  'esm-bundler': {
+    file: pkg.module,
+    format: `es`,
+  },
+  cjs: {
+    file: pkg.main,
+    format: `cjs`,
+  },
+  global: {
+    file: pkg.unpkg,
+    format: `iife`,
+  },
+  esm: {
+    file: pkg.browser,
+    format: `es`,
+  },
+}
 
-  if (format === 'iife') {
-    // config.input = 'src/entries/iife.ts'
-    config.output.file = pkg.unpkg
-    config.output.name = exportName
-  } else if (format === 'es') {
-    config.output.file = isBrowser ? pkg.browser : pkg.module
-  } else if (format === 'cjs') {
-    config.output.file = 'dist/vue-router.cjs.js'
+const allFormats = Object.keys(outputConfigs)
+// in vue-router there are not that many
+const packageFormats = allFormats
+const packageConfigs = packageFormats.map(format =>
+  createConfig(format, outputConfigs[format])
+)
+
+// only add the production ready if we are bundling the options
+packageFormats.forEach(format => {
+  if (format === 'cjs') {
+    packageConfigs.push(createProductionConfig(format))
+  } else if (format === 'global') {
+    packageConfigs.push(createMinifiedConfig(format))
   }
+})
+
+export default packageConfigs
 
-  if (!external) {
-    config.plugins.push(resolve(), commonjs())
-  } else {
-    config.external = external
+function createConfig(format, output, plugins = []) {
+  if (!output) {
+    console.log(require('chalk').yellow(`invalid format: "${format}"`))
+    process.exit(1)
   }
 
-  config.plugins.push(
-    ts({
-      // only check once, during the es version with browser (it includes external libs)
-      check: format === 'es' && isBrowser && !minify,
-      tsconfigOverride: {
-        compilerOptions: {
-          // same for d.ts files
-          declaration: format === 'es' && isBrowser && !minify,
-          module: 'esnext', // we need to override it because mocha requires this value to be commonjs
-          target: format === 'iife' || format === 'cjs' ? 'es5' : 'esnext',
-        },
+  output.sourcemap = true
+  output.banner = banner
+  output.externalLiveBindings = false
+  output.globals = { vue: 'Vue' }
+
+  const isProductionBuild = /\.prod\.js$/.test(output.file)
+  const isGlobalBuild = format === 'global'
+  const isRawESMBuild = format === 'esm'
+  const isNodeBuild = format === 'cjs'
+  const isBundlerESMBuild = /esm-bundler/.test(format)
+
+  if (isGlobalBuild) output.name = 'VueRouter'
+
+  const shouldEmitDeclarations = !hasTSChecked
+
+  const tsPlugin = ts({
+    check: !hasTSChecked,
+    tsconfig: path.resolve(__dirname, 'tsconfig.json'),
+    cacheRoot: path.resolve(__dirname, 'node_modules/.rts2_cache'),
+    tsconfigOverride: {
+      compilerOptions: {
+        sourceMap: output.sourcemap,
+        declaration: shouldEmitDeclarations,
+        declarationMap: shouldEmitDeclarations,
       },
-    })
-  )
+      exclude: ['__tests__', 'test-dts'],
+    },
+  })
+  // we only need to check TS and generate declarations once for each build.
+  // it also seems to run into weird issues when checking multiple times
+  // during a single build.
+  hasTSChecked = true
 
-  if (minify) {
-    config.plugins.push(
-      terser({
-        module: format === 'es',
-        // output: {
-        //   preamble: banner,
-        // },
-      })
-    )
-    config.output.file = config.output.file.replace(/\.js$/i, '.min.js')
+  const external = ['vue']
+
+  const nodePlugins = [resolve(), commonjs()]
+
+  return {
+    input: `src/index.ts`,
+    // Global and Browser ESM builds inlines everything so that they can be
+    // used alone.
+    external,
+    plugins: [
+      tsPlugin,
+      createReplacePlugin(
+        isProductionBuild,
+        isBundlerESMBuild,
+        // isBrowserBuild?
+        isGlobalBuild || isRawESMBuild || isBundlerESMBuild,
+        isGlobalBuild,
+        isNodeBuild
+      ),
+      ...nodePlugins,
+      ...plugins,
+    ],
+    output,
+    // onwarn: (msg, warn) => {
+    //   if (!/Circular/.test(msg)) {
+    //     warn(msg)
+    //   }
+    // },
   }
+}
 
-  return config
+function createReplacePlugin(
+  isProduction,
+  isBundlerESMBuild,
+  isBrowserBuild,
+  isGlobalBuild,
+  isNodeBuild
+) {
+  const replacements = {
+    __COMMIT__: `"${process.env.COMMIT}"`,
+    __VERSION__: `"${pkg.version}"`,
+    __DEV__: isBundlerESMBuild
+      ? // preserve to be handled by bundlers
+        `(process.env.NODE_ENV !== 'production')`
+      : // hard coded dev/prod builds
+        !isProduction,
+    // this is only used during tests
+    __TEST__: isBundlerESMBuild ? `(process.env.NODE_ENV === 'test')` : false,
+    // If the build is expected to run directly in the browser (global / esm builds)
+    __BROWSER__: isBrowserBuild,
+    // is targeting bundlers?
+    __BUNDLER__: isBundlerESMBuild,
+    __GLOBAL__: isGlobalBuild,
+    // is targeting Node (SSR)?
+    __NODE_JS__: isNodeBuild,
+  }
+  // allow inline overrides like
+  //__RUNTIME_COMPILE__=true yarn build
+  Object.keys(replacements).forEach(key => {
+    if (key in process.env) {
+      replacements[key] = process.env[key]
+    }
+  })
+  return replace(replacements)
+}
+
+function createProductionConfig(format) {
+  return createConfig(format, {
+    file: `dist/${name}.${format}.prod.js`,
+    format: outputConfigs[format].format,
+  })
 }
 
-export default [
-  // browser-friendly UMD build
-  createEntry({ format: 'iife' }),
-  createEntry({ format: 'iife', minify: true }),
-  createEntry({ format: 'cjs' }),
-  // TODO: prod vs env
-  createEntry({ format: 'es' }),
-  createEntry({ format: 'es', isBrowser: true }),
-]
+function createMinifiedConfig(format) {
+  const { terser } = require('rollup-plugin-terser')
+  return createConfig(
+    format,
+    {
+      file: `dist/${name}.${format}.prod.js`,
+      format: outputConfigs[format].format,
+    },
+    [
+      terser({
+        module: /^esm/.test(format),
+        compress: {
+          ecma: 2015,
+          pure_getters: true,
+        },
+      }),
+    ]
+  )
+}
index 07fa4e41ff477dd223b588f1fe984f91f61d49f3..69d855a79347b7bd07610d71fc5b8dcdef98d3fe 100644 (file)
 import {
-  RouteLocationNormalized,
-  RouteLocation,
-  MatcherLocationNormalized,
   MatcherLocation,
+  MatcherLocationNormalized,
+  RouteLocation,
+  RouteLocationNormalized,
 } from './types'
 
-// we could use symbols, but this is for IE9 only and there is
-// not Symbol support anyway
-const isRouterError = '__RouterError'
-
-/**
- * Generic Error coming from the Router.
- */
-export class RouterError extends Error {
-  protected __proto__: any
-  // @ts-ignore for IE inheritance support
-  private [isRouterError] = true
-
-  /**
-   * Creates a Router specific Error
-   *
-   * @param message Error Message
-   */
-  constructor(message: string) {
-    super(message)
-
-    // restore prototype chain
-    const actualProto = new.target.prototype
-
-    if (Object.setPrototypeOf) {
-      Object.setPrototypeOf(this, actualProto)
-    } else {
-      this.__proto__ = actualProto
-    }
-  }
-
-  static is(error: Error): error is RouterError {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isRouterError]
-    } else {
-      return error instanceof RouterError
-    }
-  }
-
-  get name() {
-    return this.constructor.name
-  }
+export const enum ErrorTypes {
+  MATCHER_NOT_FOUND,
+  NAVIGATION_GUARD_REDIRECT,
+  NAVIGATION_ABORTED,
+  NAVIGATION_CANCELLED,
+  // Using string enums because error codes are exposed to developers
+  // and number enums could collide with other error codes in runtime
+  // MATCHER_NOT_FOUND = 'MATCHER_NOT_FOUND',
+  // NAVIGATION_GUARD_REDIRECT = 'NAVIGATION_GUARD_REDIRECT',
+  // NAVIGATION_ABORTED = 'NAVIGATION_ABORTED',
+  // NAVIGATION_CANCELLED = 'NAVIGATION_CANCELLED',
 }
 
-const isNoRouteMatchError = '__NoRouteMatchError'
-export class NoRouteMatchError extends RouterError {
-  // @ts-ignore for IE inheritance support
-  private [isNoRouteMatchError] = true
-
-  constructor(
-    location: MatcherLocation,
-    currentLocation?: MatcherLocationNormalized
-  ) {
-    super(
-      'No match for\n' +
-        JSON.stringify(location) +
-        (currentLocation
-          ? '\nwhile being at\n' + JSON.stringify(currentLocation)
-          : '')
-    )
-  }
-
-  static is(error: Error): error is NoRouteMatchError {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isNoRouteMatchError]
-    } else {
-      return error instanceof NoRouteMatchError
-    }
-  }
+interface RouterErrorBase extends Error {
+  type: ErrorTypes
 }
 
-const isInvalidRouteMatch = '__InvalidRouteMatch'
-/**
- * Error used when the matcher fails to resolve a location
- */
-export class InvalidRouteMatch extends RouterError {
-  // @ts-ignore for IE inheritance support
-  private [isNoRouteMatchError] = true
-
-  constructor(location: any) {
-    // TODO: improve the error to include currentLocation and use it for more cases
-    super(
-      `Cannot redirect using a relative location:\n${stringifyRoute(
-        location
-      )}\nUse the function redirect and explicitly provide a name`
-    )
-  }
-
-  static is(error: Error): error is InvalidRouteMatch {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isInvalidRouteMatch]
-    } else {
-      return error instanceof InvalidRouteMatch
-    }
-  }
+export interface MatcherError extends RouterErrorBase {
+  type: ErrorTypes.MATCHER_NOT_FOUND
+  location: MatcherLocation
+  currentLocation?: MatcherLocationNormalized
 }
 
-const isNavigationGuardRedirect = '__NavigationGuardRedirect'
-/**
- * Error used when rejecting a navigation because of a redirection. Contains
- * information about where we where trying to go and where we are going instead
- */
-export class NavigationGuardRedirect extends RouterError {
-  // @ts-ignore for IE inheritance support
-  private [isNoRouteMatchError] = true
-
-  to: RouteLocation
+export interface NavigationError extends RouterErrorBase {
+  type: ErrorTypes.NAVIGATION_ABORTED | ErrorTypes.NAVIGATION_CANCELLED
   from: RouteLocationNormalized
-  // TODO: refactor order of arguments
-  // TODO: refactor into parent class NavigationError
-  constructor(from: RouteLocationNormalized, to: RouteLocation) {
-    super(
-      `Redirected from "${from.fullPath}" to "${stringifyRoute(
-        to
-      )}" via a navigation guard`
-    )
-
-    this.from = from
-    this.to = to
-  }
-
-  static is(error: Error): error is NavigationGuardRedirect {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isNavigationGuardRedirect]
-    } else {
-      return error instanceof NavigationGuardRedirect
-    }
-  }
-}
-
-const isNavigationAborted = '__NavigationAborted'
-/**
- * Navigation aborted by next(false)
- */
-export class NavigationAborted extends RouterError {
-  // @ts-ignore for IE inheritance support
-  private [isNavigationAborted] = true
-
   to: RouteLocationNormalized
-  from: RouteLocationNormalized
-  constructor(to: RouteLocationNormalized, from: RouteLocationNormalized) {
-    super(
-      `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard`
-    )
-
-    this.from = from
-    this.to = to
-  }
-
-  static is(error: Error): error is NavigationAborted {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isNavigationAborted]
-    } else {
-      return error instanceof NavigationAborted
-    }
-  }
 }
 
-const isNavigationCancelled = '__NavigationCancelled'
-/**
- * Navigation canceled by the user by pushing/replacing a new location
- * TODO: is the name good?
- */
-// @ts-ignore RouterError is a constructor
-export class NavigationCancelled extends RouterError {
-  // @ts-ignore for IE inheritance support
-  private [isNavigationCancelled] = true
-
-  to: RouteLocationNormalized
-  from: RouteLocationNormalized
-  constructor(to: RouteLocationNormalized, from: RouteLocationNormalized) {
-    super(
-      `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new \`push\` or \`replace\``
-    )
+export interface NavigationRedirectError
+  extends Omit<NavigationError, 'to' | 'type'> {
+  type: ErrorTypes.NAVIGATION_GUARD_REDIRECT
+  to: RouteLocation
+}
 
-    this.from = from
-    this.to = to
-  }
+// DEV only debug messages
+const ErrorTypeMessages = {
+  [ErrorTypes.MATCHER_NOT_FOUND]({ location, currentLocation }: MatcherError) {
+    return `No match for\n ${JSON.stringify(location)}${
+      currentLocation
+        ? '\nwhile being at\n' + JSON.stringify(currentLocation)
+        : ''
+    }`
+  },
+  [ErrorTypes.NAVIGATION_GUARD_REDIRECT]({
+    from,
+    to,
+  }: NavigationRedirectError) {
+    return `Redirected from "${from.fullPath}" to "${stringifyRoute(
+      to
+    )}" via a navigation guard`
+  },
+  [ErrorTypes.NAVIGATION_ABORTED]({ from, to }: NavigationError) {
+    return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard`
+  },
+  [ErrorTypes.NAVIGATION_CANCELLED]({ from, to }: NavigationError) {
+    return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new \`push\` or \`replace\``
+  },
+}
 
-  static is(error: Error): error is NavigationCancelled {
-    // only IE9 seems to break the inheritance chain
-    // and set Error as the name
-    if (error.name === 'Error') {
-      // @ts-ignore for IE inheritance support
-      return error[isNavigationCancelled]
-    } else {
-      return error instanceof NavigationCancelled
-    }
+// Possible internal errors
+type RouterError = NavigationError | NavigationRedirectError | MatcherError
+// Public errors, TBD
+//  export type PublicRouterError = NavigationError
+
+export function createRouterError<E extends RouterError>(
+  type: E['type'],
+  params: Omit<E, 'type' | keyof Error>
+): E {
+  if (__DEV__ || !__BROWSER__) {
+    return Object.assign(
+      new Error(ErrorTypeMessages[type](params as any)),
+      { type },
+      params
+    ) as E
+  } else {
+    return Object.assign(new Error(), { type }, params) as E
   }
 }
 
-const propertiesToLog: (keyof RouteLocationNormalized)[] = [
-  'params',
-  'query',
-  'hash',
-]
+const propertiesToLog = ['params', 'query', 'hash'] as const
 
 function stringifyRoute(to: RouteLocation): string {
   if (typeof to === 'string') return to
   if ('path' in to) return to.path
-  const location: Partial<RouteLocationNormalized> = {}
+  const location = {} as Record<string, unknown>
   for (const key of propertiesToLog) {
-    // @ts-ignore
     if (key in to) location[key] = to[key]
   }
   return JSON.stringify(location, null, 2)
index ca93dc0d646c7df536ba467fb1bef7227c3b7c83..d276eaf920ba5fd9804b5ddde55458e56def28c7 100644 (file)
@@ -1,2 +1,3 @@
 // Global compile-time constants
 declare var __DEV__: boolean
+declare var __BROWSER__: boolean
index e5058595da93ef0e6e592e7a7a2d746612141e20..4bccbb7b2a631b5750d087b42fb04ad14ca841c3 100644 (file)
@@ -4,11 +4,10 @@ import createWebHashHistory from './history/hash'
 
 export {
   RouteLocationNormalized,
-  START_LOCATION_NORMALIZED as START_LOCATION,
-  // needed for types, should probably be removed by changing the
   RouteLocationOptions,
+  START_LOCATION_NORMALIZED as START_LOCATION,
 } from './types'
-export { createRouter, Router } from './router'
+export { createRouter, Router, RouterOptions } from './router'
 
 export { onBeforeRouteLeave } from './navigationGuards'
 export { useRoute, useRouter } from './injectKeys'
index 002dec0c5b8811fcb6300501ce617a617e11dd06..e4fe9642c61bca36f991cc53c87dbe045bbef818 100644 (file)
@@ -4,7 +4,7 @@ import {
   MatcherLocationNormalized,
   ListenerRemover,
 } from '../types'
-import { NoRouteMatchError } from '../errors'
+import { createRouterError, ErrorTypes, MatcherError } from '../errors'
 import { createRouteRecordMatcher, RouteRecordMatcher } from './path-matcher'
 import { RouteRecordNormalized } from './types'
 import {
@@ -188,7 +188,10 @@ export function createRouterMatcher(
     if ('name' in location && location.name) {
       matcher = matcherMap.get(location.name)
 
-      if (!matcher) throw new NoRouteMatchError(location)
+      if (!matcher)
+        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
+          location,
+        })
 
       name = matcher.record.name
       // TODO: merge params with current location. Should this be done by name. I think there should be some kind of relationship between the records like children of a parent should keep parent props but not the rest
@@ -214,7 +217,11 @@ export function createRouterMatcher(
       matcher = currentLocation.name
         ? matcherMap.get(currentLocation.name)
         : matchers.find(m => m.re.test(currentLocation.path))
-      if (!matcher) throw new NoRouteMatchError(location, currentLocation)
+      if (!matcher)
+        throw createRouterError<MatcherError>(ErrorTypes.MATCHER_NOT_FOUND, {
+          location,
+          currentLocation,
+        })
       name = matcher.record.name
       params = location.params || currentLocation.params
       path = matcher.stringify(params)
index 4c2f879168d71bec1552ba89f6d4a61dcd3f1155..54cd0b568d10518a6f7e3c621eec5392386182eb 100644 (file)
@@ -19,11 +19,7 @@ import {
   scrollToPosition,
 } from './utils/scroll'
 import { createRouterMatcher } from './matcher'
-import {
-  NavigationCancelled,
-  NavigationGuardRedirect,
-  NavigationAborted,
-} from './errors'
+import { createRouterError, ErrorTypes, NavigationError } from './errors'
 import {
   extractComponentsGuards,
   guardToPromiseFn,
@@ -218,19 +214,23 @@ export function createRouter({
     try {
       await navigate(toLocation, from)
     } catch (error) {
-      if (NavigationGuardRedirect.is(error)) {
-        // push was called while waiting in guards
-        if (pendingLocation !== toLocation) {
-          triggerError(new NavigationCancelled(toLocation, from))
-        }
+      // push was called while waiting in guards
+      // TODO: write tests
+      if (pendingLocation !== toLocation) {
+        triggerError(
+          createRouterError<NavigationError>(ErrorTypes.NAVIGATION_CANCELLED, {
+            from,
+            to: toLocation,
+          })
+        )
+      }
+
+      if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
         // preserve the original redirectedFrom if any
         return pushWithRedirect(error.to, redirectedFrom || toLocation)
-      } else {
-        // TODO: write tests
-        if (pendingLocation !== toLocation) {
-          triggerError(new NavigationCancelled(toLocation, from))
-        }
       }
+
+      // unkwnown error
       triggerError(error)
     }
 
@@ -345,7 +345,13 @@ export function createRouter({
   ) {
     // a more recent navigation took place
     if (pendingLocation !== toLocation) {
-      return triggerError(new NavigationCancelled(toLocation, from), isPush)
+      return triggerError(
+        createRouterError<NavigationError>(ErrorTypes.NAVIGATION_CANCELLED, {
+          from,
+          to: toLocation,
+        }),
+        isPush
+      )
     }
 
     // remove registered guards from removed matched records
@@ -397,19 +403,28 @@ export function createRouter({
         false
       )
     } catch (error) {
-      if (NavigationGuardRedirect.is(error)) {
+      if (error.type === ErrorTypes.NAVIGATION_GUARD_REDIRECT) {
         // 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, from), false)
+          return triggerError(
+            createRouterError<NavigationError>(
+              ErrorTypes.NAVIGATION_CANCELLED,
+              {
+                from,
+                to: toLocation,
+              }
+            ),
+            false
+          )
         }
         triggerError(error, false)
 
         // the error is already handled by router.push
         // we just want to avoid logging the error
         pushWithRedirect(error.to, toLocation).catch(() => {})
-      } else if (NavigationAborted.is(error)) {
+      } else if (error.type === ErrorTypes.NAVIGATION_ABORTED) {
         console.log('Cancelled, going to', -info.distance)
         // TODO: test on different browsers ensure consistent behavior
         history.go(-info.distance, false)
index 29bf6e6fde41bb9a5908ee44a0d7e375c30a7995..7d1f7c9ffa53a84e7a10aacfe936e3857f770c46 100644 (file)
@@ -8,7 +8,12 @@ import {
 } from '../types'
 
 import { isRouteLocation } from '../types'
-import { NavigationGuardRedirect, NavigationAborted } from '../errors'
+import {
+  createRouterError,
+  ErrorTypes,
+  NavigationError,
+  NavigationRedirectError,
+} from '../errors'
 
 export function guardToPromiseFn(
   guard: NavigationGuard,
@@ -21,9 +26,23 @@ export function guardToPromiseFn(
       const next: NavigationGuardCallback = (
         valid?: boolean | RouteLocation | NavigationGuardNextCallback
       ) => {
-        if (valid === false) reject(new NavigationAborted(to, from))
+        if (valid === false)
+          reject(
+            createRouterError<NavigationError>(ErrorTypes.NAVIGATION_ABORTED, {
+              from,
+              to,
+            })
+          )
         else if (isRouteLocation(valid)) {
-          reject(new NavigationGuardRedirect(to, valid))
+          reject(
+            createRouterError<NavigationRedirectError>(
+              ErrorTypes.NAVIGATION_GUARD_REDIRECT,
+              {
+                from: to,
+                to: valid,
+              }
+            )
+          )
         } else if (!valid || valid === true) {
           resolve()
         } else {
index e9fd80fe1fd5cfb8fbda5273a5b15e35ca211a13..fd5c1f060f9f347cc160f57006dad84bf7936cd8 100644 (file)
@@ -1,23 +1,31 @@
 {
   "include": ["src/global.d.ts", "src/**/*.ts", "__tests__/**/*.ts"],
   "compilerOptions": {
-    "target": "esnext",
-    "module": "commonjs",
-    // "lib": ["es2017.object"] /* Specify library files to be included in the compilation. */,
-    "declaration": true,
-    "declarationMap": true,
-    "sourceMap": true,
-    "outDir": "./dist",
-    "removeComments": false,
+    "baseUrl": ".",
+    "rootDir": ".",
+    "outDir": "dist",
+    "sourceMap": false,
     "noEmit": true,
 
-    "strict": true,
+    "target": "esnext",
+    "module": "esnext",
+    "moduleResolution": "node",
+    "allowJs": false,
 
     "noUnusedLocals": true,
-    // "noUnusedParameters": true,
+    "strictNullChecks": true,
+    "noImplicitAny": true,
+    "noImplicitThis": true,
     "noImplicitReturns": true,
+    "strict": true,
+    "isolatedModules": false,
 
-    "moduleResolution": "node",
-    "esModuleInterop": true
+    "experimentalDecorators": true,
+    "resolveJsonModule": true,
+    "esModuleInterop": true,
+    "removeComments": false,
+    "jsx": "preserve",
+    "lib": ["esnext", "dom"],
+    "types": ["jest", "node"]
   }
 }