]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix(types): stricter meta with required fields
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 1 Jul 2024 08:42:00 +0000 (10:42 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 1 Jul 2024 08:42:08 +0000 (10:42 +0200)
packages/router/src/index.ts
packages/router/src/types/index.ts
packages/router/test-dts/components.test-d.tsx
packages/router/test-dts/legacy.test-d.ts
packages/router/test-dts/meta.test-d.ts
packages/router/test-dts/navigationGuards.test-d.ts
packages/router/test-dts/routeRecords.test-d.ts
packages/router/test-dts/typed-routes.test-d.ts
packages/router/tsconfig.json
packages/router/vitest.config.ts

index 2a62ad15670c4d4f21d863a81b4cf2cace137422..089cfe0a7e9586d04990632fe69bc57eb457d42c 100644 (file)
@@ -59,6 +59,7 @@ export type {
   RouteRecordMultipleViewsWithChildren,
   RouteRecordRedirect,
   RouteMeta,
+  _RouteMetaBase,
   RouteComponent,
   // RawRouteComponent,
   RouteParamsGeneric,
index e3069740b0d806c03ed97060c2b5d191e2730485..3508f3a6cf19bd397c5771c80d29c05379e53a32 100644 (file)
@@ -189,7 +189,9 @@ export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
 /**
  * Internal type for common properties among all kind of {@link RouteRecordRaw}.
  */
-export interface _RouteRecordBase extends PathParserOptions {
+export interface _RouteRecordBase
+  extends PathParserOptions,
+    _RouteRecordBaseMeta {
   /**
    * Path of the record. Should start with `/` unless the record is the child of
    * another record.
@@ -228,7 +230,7 @@ export interface _RouteRecordBase extends PathParserOptions {
   /**
    * Arbitrary data attached to the record.
    */
-  meta?: RouteMeta
+  // meta?: RouteMeta
 
   /**
    * Array of nested routes.
@@ -241,6 +243,12 @@ export interface _RouteRecordBase extends PathParserOptions {
   props?: _RouteRecordProps | Record<string, _RouteRecordProps>
 }
 
+/**
+ * Default type for RouteMeta when not augmented.
+ * @internal
+ */
+export type _RouteMetaBase = Record<string | number | symbol, unknown>
+
 /**
  * Interface to type `meta` fields in route records.
  *
@@ -257,7 +265,33 @@ export interface _RouteRecordBase extends PathParserOptions {
  *  }
  * ```
  */
-export interface RouteMeta extends Record<string | number | symbol, unknown> {}
+export interface RouteMeta extends _RouteMetaBase {}
+
+/**
+ * Returns `true` if the passed `RouteMeta` type hasn't been augmented. Return `false` otherwise.
+ * @internal
+ */
+export type IsRouteMetaBase<RM> = _RouteMetaBase extends RM ? true : false
+/**
+ * Returns `true` if the passed `RouteMeta` type has been augmented with required fields. Return `false` otherwise.
+ * @internal
+ */
+export type IsRouteMetaRequired<RM> = Partial<RM> extends RM ? false : true
+
+export type _RouteRecordBaseMeta = IsRouteMetaRequired<RouteMeta> extends true
+  ? {
+      /**
+       * Arbitrary data attached to the record. Required because the `RouteMeta` type has been augmented with required
+       * fields.
+       */
+      meta: RouteMeta
+    }
+  : {
+      /**
+       * Arbitrary data attached to the record.
+       */
+      meta?: RouteMeta
+    }
 
 /**
  * Route Record defining one single component with the `component` option.
index 532f66bac4248f4365e5932630e48fea013e9f16..07c2705204d8cbe2796198c9eba15a808d6bb2ea 100644 (file)
@@ -5,27 +5,32 @@ import {
   createRouter,
   createMemoryHistory,
 } from './index'
-import { expectTypeOf } from 'vitest'
+import { it, describe, expectTypeOf } from 'vitest'
 
-let router = createRouter({
-  history: createMemoryHistory(),
-  routes: [],
-})
+describe('Components', () => {
+  let router = createRouter({
+    history: createMemoryHistory(),
+    routes: [],
+  })
 
-// RouterLink
-// @ts-expect-error missing to
-expectError(<RouterLink />)
-// @ts-expect-error: invalid prop
-expectError(<RouterLink to="/" custom="text" />)
-// @ts-expect-error: invalid prop
-expectError(<RouterLink to="/" replace="text" />)
-expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
-expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
-expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
-expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
-expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)
+  // TODO: split into multiple tests
+  it('works', () => {
+    // RouterLink
+    // @ts-expect-error missing to
+    expectError(<RouterLink />)
+    // @ts-expect-error: invalid prop
+    expectError(<RouterLink to="/" custom="text" />)
+    // @ts-expect-error: invalid prop
+    expectError(<RouterLink to="/" replace="text" />)
+    expectTypeOf<JSX.Element>(<RouterLink to="/foo" replace />)
+    expectTypeOf<JSX.Element>(<RouterLink to="/foo" />)
+    expectTypeOf<JSX.Element>(<RouterLink class="link" to="/foo" />)
+    expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} />)
+    expectTypeOf<JSX.Element>(<RouterLink to={{ path: '/foo' }} custom />)
 
-// RouterView
-expectTypeOf<JSX.Element>(<RouterView class="view" />)
-expectTypeOf<JSX.Element>(<RouterView name="foo" />)
-expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
+    // RouterView
+    expectTypeOf<JSX.Element>(<RouterView class="view" />)
+    expectTypeOf<JSX.Element>(<RouterView name="foo" />)
+    expectTypeOf<JSX.Element>(<RouterView route={router.currentRoute.value} />)
+  })
+})
index 8f129a7e612b5c835ea1b00993678fec2079ff31..3b5f70b57c5ba9dcb6679e16472a980a5cbad09c 100644 (file)
@@ -1,12 +1,32 @@
-import { expectTypeOf } from 'vitest'
-import { Router, RouteLocationNormalizedLoaded } from './index'
+import { describe, expectTypeOf, it } from 'vitest'
+import {
+  useRouter,
+  useRoute,
+  // rename types for better error messages, otherwise they have the same name
+  // RouteLocationNormalizedLoadedTyped as I_RLNLT
+} from './index'
 import { defineComponent } from 'vue'
 
-defineComponent({
-  methods: {
-    doStuff() {
-      expectTypeOf<Router>(this.$router)
-      expectTypeOf<RouteLocationNormalizedLoaded>(this.$route)
-    },
-  },
+describe('Instance types', () => {
+  it('creates a $route instance property', () => {
+    defineComponent({
+      methods: {
+        doStuff() {
+          // TODO: can't do a proper check because of typed routes
+          expectTypeOf(this.$route.params).toMatchTypeOf(useRoute().params)
+        },
+      },
+    })
+  })
+
+  it('creates $router instance properties', () => {
+    defineComponent({
+      methods: {
+        doStuff() {
+          // TODO: can't do a proper check because of typed routes
+          expectTypeOf(this.$router.back).toEqualTypeOf(useRouter().back)
+        },
+      },
+    })
+  })
 })
index 32e2010faf936578e5cde8ac3432808e689f356f..95aa46101dee76032046cc7e7d46661554792b24 100644 (file)
@@ -4,10 +4,11 @@ import { describe, it, expectTypeOf } from 'vitest'
 
 const component = defineComponent({})
 
-declare module './index' {
+declare module '.' {
   interface RouteMeta {
     requiresAuth?: boolean
-    nested: { foo: string }
+    // TODO: it would be nice to be able to test required meta without polluting all tests
+    nested?: { foo: string }
   }
 }
 
@@ -27,14 +28,18 @@ describe('RouteMeta', () => {
             },
           },
         },
-        {
-          path: '/foo',
-          component,
-          // @ts-expect-error
-          meta: {},
-        },
       ],
     })
+
+    router.addRoute({
+      path: '/foo',
+      component,
+      meta: {
+        nested: {
+          foo: 'foo',
+        },
+      },
+    })
   })
 
   it('route location in guards', () => {
@@ -43,9 +48,12 @@ describe('RouteMeta', () => {
       routes: [],
     })
     router.beforeEach(to => {
-      expectTypeOf<{ requiresAuth?: Boolean; nested: { foo: string } }>(to.meta)
+      expectTypeOf<{ requiresAuth?: Boolean; nested?: { foo: string } }>(
+        to.meta
+      )
       expectTypeOf<unknown>(to.meta.lol)
-      if (to.meta.nested.foo == 'foo' || to.meta.lol) return false
+      if (to.meta.nested?.foo == 'foo' || to.meta.lol) return false
+      return
     })
   })
 })
index 6bf24ed8a7822e122c55cadc2b3636c9336452f7..86f4a0c4a19e753448ce5618494edea261e271d4 100644 (file)
@@ -1,4 +1,4 @@
-import { expectTypeOf } from 'vitest'
+import { expectTypeOf, describe, it } from 'vitest'
 import {
   createRouter,
   createWebHistory,
@@ -14,44 +14,49 @@ const router = createRouter({
   routes: [],
 })
 
-router.beforeEach((to, from) => {
-  return { path: '/' }
-})
-
-router.beforeEach((to, from) => {
-  return '/'
-})
-
-router.beforeEach((to, from) => {
-  return false
-})
-
-router.beforeEach((to, from, next) => {
-  next(undefined)
-})
-
-// @ts-expect-error
-router.beforeEach((to, from, next) => {
-  return Symbol('not supported')
-})
-// @ts-expect-error
-router.beforeEach(() => {
-  return Symbol('not supported')
-})
-
-router.beforeEach((to, from, next) => {
-  // @ts-expect-error
-  next(Symbol('not supported'))
-})
-
-router.afterEach((to, from, failure) => {
-  expectTypeOf<NavigationFailure | undefined | void>(failure)
-  if (isNavigationFailure(failure)) {
-    expectTypeOf<RouteLocationNormalized>(failure.from)
-    expectTypeOf<RouteLocationRaw>(failure.to)
-  }
-  if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
-    expectTypeOf<RouteLocationNormalized>(failure.from)
-    expectTypeOf<RouteLocationRaw>(failure.to)
-  }
+describe('Navigation guards', () => {
+  // TODO: split into multiple tests
+  it('works', () => {
+    router.beforeEach((to, from) => {
+      return { path: '/' }
+    })
+
+    router.beforeEach((to, from) => {
+      return '/'
+    })
+
+    router.beforeEach((to, from) => {
+      return false
+    })
+
+    router.beforeEach((to, from, next) => {
+      next(undefined)
+    })
+
+    // @ts-expect-error
+    router.beforeEach((to, from, next) => {
+      return Symbol('not supported')
+    })
+    // @ts-expect-error
+    router.beforeEach(() => {
+      return Symbol('not supported')
+    })
+
+    router.beforeEach((to, from, next) => {
+      // @ts-expect-error
+      next(Symbol('not supported'))
+    })
+
+    router.afterEach((to, from, failure) => {
+      expectTypeOf<NavigationFailure | undefined | void>(failure)
+      if (isNavigationFailure(failure)) {
+        expectTypeOf<RouteLocationNormalized>(failure.from)
+        expectTypeOf<RouteLocationRaw>(failure.to)
+      }
+      if (isNavigationFailure(failure, NavigationFailureType.cancelled)) {
+        expectTypeOf<RouteLocationNormalized>(failure.from)
+        expectTypeOf<RouteLocationRaw>(failure.to)
+      }
+    })
+  })
 })
index e806dad1c487fec0280dc14d9f665204b7462e0e..86cac2e686e49934dad6147fc9b636511634d762 100644 (file)
@@ -1,4 +1,5 @@
-import { RouteRecordRaw } from './index'
+import { describe, it } from 'vitest'
+import type { RouteLocationNormalized, RouteRecordRaw } from './index'
 import { defineComponent } from 'vue'
 
 const component = defineComponent({})
@@ -6,56 +7,72 @@ const components = { default: component }
 
 const routes: RouteRecordRaw[] = []
 
-routes.push({ path: '/', redirect: '/foo' })
+describe('RouteRecords', () => {
+  // TODO: split into multiple tests
+  it('works', () => {
+    routes.push({ path: '/', redirect: '/foo' })
 
-// @ts-expect-error cannot have components and component at the same time
-routes.push({ path: '/', components, component })
+    // @ts-expect-error cannot have components and component at the same time
+    routes.push({ path: '/', components, component })
 
-// a redirect record with children to point to a child
-routes.push({
-  path: '/',
-  redirect: '/foo',
-  children: [
-    {
-      path: 'foo',
+    // a redirect record with children to point to a child
+    routes.push({
+      path: '/',
+      redirect: '/foo',
+      children: [
+        {
+          path: 'foo',
+          component,
+        },
+      ],
+    })
+
+    // same but with a nested route
+    routes.push({
+      path: '/',
       component,
-    },
-  ],
-})
+      redirect: '/foo',
+      children: [
+        {
+          path: 'foo',
+          component,
+        },
+      ],
+    })
 
-// same but with a nested route
-routes.push({
-  path: '/',
-  component,
-  redirect: '/foo',
-  children: [
-    {
-      path: 'foo',
+    routes.push({ path: '/a/b', component, props: true })
+    routes.push({
+      path: '/a/b',
       component,
-    },
-  ],
-})
+      props: (to: RouteLocationNormalized<'/[id]+'>) => to.params.id,
+    })
+    // @ts-expect-error: props should be an object
+    routes.push({ path: '/a/b', components, props: to => to.params.id })
+    routes.push({
+      path: '/a/b',
+      components,
+      props: {
+        default: (to: RouteLocationNormalized<'/[id]+'>) => to.params.id,
+      },
+    })
+    routes.push({ path: '/', components, props: true })
 
-routes.push({ path: '/', component, props: true })
-routes.push({ path: '/', component, props: to => to.params.id })
-// @ts-expect-error: props should be an object
-routes.push({ path: '/', components, props: to => to.params.id })
-routes.push({ path: '/', components, props: { default: to => to.params.id } })
-routes.push({ path: '/', components, props: true })
-
-// let r: RouteRecordRaw = {
-//   path: '/',
-//   component,
-//   components,
-// }
-
-export function filterNestedChildren(children: RouteRecordRaw[]) {
-  return children.filter(r => {
-    if (r.redirect) {
-      r.children?.map(() => {})
-    }
-    if (r.children) {
-      r.children = filterNestedChildren(r.children)
+    // let r: RouteRecordRaw = {
+    //   path: '/',
+    //   component,
+    //   components,
+    // }
+
+    function filterNestedChildren(children: RouteRecordRaw[]) {
+      return children.filter(r => {
+        if (r.redirect) {
+          r.children?.map(() => {})
+        }
+        if (r.children) {
+          r.children = filterNestedChildren(r.children)
+        }
+      })
     }
+    filterNestedChildren(routes)
   })
-}
+})
index e12bdab2aea342793287f7eef845330013a48c08..7f68498189e4a3b436f22017b41a084ea9791979 100644 (file)
@@ -10,7 +10,7 @@ import {
 
 // type is needed instead of an interface
 // https://github.com/microsoft/TypeScript/issues/15300
-type RouteMap = {
+export type RouteMap = {
   '/[...path]': RouteRecordInfo<
     '/[...path]',
     '/:path(.*)',
index 22219c6f01a5b6670ae20ddd64bb33c7b00deee4..41fc6c37882812ee6833e8aefe781935b573ecf5 100644 (file)
@@ -2,7 +2,8 @@
   "include": [
     "src/global.d.ts",
     "src/**/*.ts",
-    "__tests__/**/*.ts"
+    "__tests__/**/*.ts",
+    "test-dts/**/*.ts"
   ],
   "compilerOptions": {
     "baseUrl": ".",
@@ -12,7 +13,7 @@
     "noEmit": true,
     "target": "esnext",
     "module": "esnext",
-    "moduleResolution": "node",
+    "moduleResolution": "Bundler",
     "allowJs": false,
     "noUnusedLocals": true,
     "strictNullChecks": true,
index bb42313f71ab9d69e74449000367a2742f454683..1e60c7351196da01fabeacaed3eee836f4670f0e 100644 (file)
@@ -32,7 +32,7 @@ export default defineConfig({
       checker: 'vue-tsc',
       // only: true,
       // by default it includes all specs too
-      include: ['**/*.test-d.ts'],
+      // include: ['**/*.test-d.ts'],
 
       // tsconfig: './tsconfig.typecheck.json',
     },