]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
refactor(types): simplify param parsing
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 13 Jun 2022 16:44:16 +0000 (18:44 +0200)
committerEduardo San Martin Morote <posva@users.noreply.github.com>
Thu, 30 Jun 2022 07:59:00 +0000 (09:59 +0200)
packages/playground/src/router.ts
packages/router/src/types/index.ts
packages/router/src/types/paths.ts
packages/router/test-dts/paths.test-d.ts
packages/router/test-dts/perfNamedRoutes.test-d.ts [new file with mode: 0644]

index c4c1b6104e4550f1947e46f75f287c116e9e13ab..ca88c525b903a702a669eec70768708a9284df3d 100644 (file)
@@ -52,7 +52,7 @@ export const router = createRouter({
       meta: { transition: 'slide-left' },
       component: async () => {
         await delay(500)
-        return component()
+        return component
       },
     },
     {
index 291c0b90283f4f843a63574b112231a59366b58a..f265b2c264f94c7bd2cee3fe25cbcf1ce9171179 100644 (file)
@@ -151,10 +151,13 @@ export type RouteLocationNamedRaw<
   ? // allows assigning a RouteLocationRaw to RouteLocationNamedRaw
     RouteQueryAndHash & LocationAsRelativeRaw & RouteLocationOptions
   : {
-      [K in Extract<keyof RouteMap, RouteRecordName>]: RouteQueryAndHash &
-        LocationAsRelativeRaw<K, RouteMap[K]> &
-        RouteLocationOptions
-    }[Extract<keyof RouteMap, RouteRecordName>]
+      [K in Extract<keyof RouteMap, RouteRecordName>]: LocationAsRelativeRaw<
+        K,
+        RouteMap[K]
+      >
+    }[Extract<keyof RouteMap, RouteRecordName>] &
+      RouteQueryAndHash &
+      RouteLocationOptions
 
 /**
  * Route Location that can infer the possible paths.
index e2318e346ffab5853f6ce819d9b2b8706481a50d..25fe6cb027c621b5c045d6c4dcb2a20b2064e80c 100644 (file)
@@ -8,23 +8,15 @@ import { RouteParams, RouteParamsRaw, RouteParamValueRaw } from '.'
  * type P = ParamsFromPath<'/:id/b/:c*'> // { id: string; c?: string[] }
  * ```
  */
-export type ParamsFromPath<P extends string = string> = string extends P
-  ? RouteParams // Generic version
-  : _ExtractParamsPath<_RemoveRegexpFromParam<P>, false> extends Record<
-      any,
-      never
-    >
-  ? Record<any, never>
-  : _ExtractParamsPath<_RemoveRegexpFromParam<P>, false>
-
-export type ParamsRawFromPath<P extends string = string> = string extends P
-  ? RouteParamsRaw // Generic version
-  : _ExtractParamsPath<_RemoveRegexpFromParam<P>, true> extends Record<
-      any,
-      never
-    >
-  ? Record<any, never>
-  : _ExtractParamsPath<_RemoveRegexpFromParam<P>, true>
+export type ParamsFromPath<P extends string = string> =
+  P extends `${string}:${string}`
+    ? Simplify<_ExtractParamsOfPath<P, false>>
+    : Record<any, never>
+
+export type ParamsRawFromPath<P extends string = string> =
+  P extends `${string}:${string}`
+    ? Simplify<_ExtractParamsOfPath<P, true>>
+    : Record<any, never>
 
 /**
  * Possible param modifiers.
@@ -57,6 +49,7 @@ export type _ParamDelimiter =
   | '@'
   | '['
   | ']'
+  | '$'
   | _ParamModifier
 
 /**
@@ -78,6 +71,164 @@ export type _ExtractParamsPath<
       _ExtractParamsPath<Rest, isRaw>
   : {}
 
+type _PathParam<P extends string, Rest extends string = ''> =
+  | `${string}:${P}`
+  | `${string}:${P}${_ParamModifier}${Rest}`
+
+type b = '/' extends _PathParam<infer P> ? P : never
+type c = '/home' extends _PathParam<infer P> ? P : never
+type d = '/user/:id' extends _PathParam<infer P, infer Rest> ? [P, Rest] : never
+type e = '/user/:id+' extends _PathParam<infer P> ? P : never
+
+export type _ExtractParamsOfPath<
+  P extends string,
+  isRaw extends boolean
+> = P extends `${string}:${infer HasParam}`
+  ? _ParamName<HasParam> extends _ParamExtractResult<
+      infer ParamName,
+      infer Rest
+    >
+    ? // ParamName is delimited by something eg: /:id/b/:c
+      // let's first remove the regex if there is one then extract the modifier
+      _ExtractModifier<_StripRegex<Rest>> extends _ModifierExtracTResult<
+        infer Modifier,
+        infer Rest2
+      >
+      ? _ParamToObject<ParamName, Modifier, isRaw> &
+          _ExtractParamsOfPath<Rest2, isRaw>
+      : {
+          NO: 1 // this should never happen as the modifier can be empty
+        }
+    : // Nothing after the param: /:id, we are done
+      _ParamToObject<HasParam, '', isRaw>
+  : {
+      // EMPTY: 1
+    }
+
+type a1 = _ExtractParamsOfPath<'/', false>
+type a2 = _ExtractParamsOfPath<'/:id', false>
+type a3 = _ExtractParamsOfPath<'/:id/:b', false>
+type a4 = _ExtractParamsOfPath<'/:id(.*)', false>
+type a5 = _ExtractParamsOfPath<'/:id(.*)/other', false>
+type a6 = _ExtractParamsOfPath<'/:id(.*)+', false>
+type a7 = _ExtractParamsOfPath<'/:id(.*)+/other', false>
+type a8 = _ExtractParamsOfPath<'/:id(.*)+/other/:b/:c/:d', false>
+
+// TODO: perf test this to see if worth because it's way more readable
+// also move to utils
+export type Simplify<T> = { [K in keyof T]: T[K] }
+
+type test1 =
+  '/:id/:b' extends `${string}:${infer P}${_ParamDelimiter}${infer Rest}`
+    ? [P, Rest]
+    : never
+
+type _ParamName_OLD<P extends string> =
+  P extends `${_AlphaNumeric}${infer Rest}`
+    ? P extends `${infer C}${Rest}`
+      ? // Keep extracting other alphanumeric chars
+        `${C}${_ParamName_OLD<Rest>}`
+      : never // ERR
+    : // add the rest to the end after a % which is invalid in a path so it can be used as a delimiter
+      ` % ${P}`
+
+interface _ParamExtractResult<P extends string, Rest extends string> {
+  param: P
+  rest: Rest
+}
+
+type _ParamName<
+  Tail extends string,
+  Head extends string = ''
+> = Tail extends `${_AlphaNumeric}${infer Rest}`
+  ? Tail extends `${infer C}${Rest}`
+    ? // Keep extracting other alphanumeric chars
+      _ParamName<Rest, `${Head}${C}`>
+    : never // ERR
+  : // add the rest to the end after a % which is invalid in a path so it can be used as a delimiter
+    _ParamExtractResult<Head, Tail>
+
+type p1 = _ParamName<'id'>
+type p2 = _ParamName<'abc+/dos'>
+type p3 = _ParamName<'abc/:dos)'>
+
+/**
+ * We consider a what comes after a param, e.g. For `/:id(\\d+)+/edit`, it would be `(\\d+)+/edit`. This should output
+ * everything after the regex while handling escaped `)`: `+/edit`.
+ */
+export type _StripRegex<S extends string> =
+  // do we have an escaped closing parenthesis?
+  S extends `${infer A}\\)${infer Rest}`
+    ? // the actual regexp finished before, A has no escaped )
+      A extends `${string})${infer Rest2}`
+      ? // get the modifier if there is one
+        `${Rest2}\\)${Rest}` // job done
+      : _RemoveUntilClosingPar<Rest> // we keep removing
+    : // simple case with no escaping
+    S extends `${string})${infer Rest}`
+    ? // extract the modifier if there is one
+      Rest
+    : // nothing to remove
+      S
+
+type r1 = _StripRegex<'(\\d+)+/edit/:other(.*)*'>
+type r3 = _StripRegex<'(.*)*'>
+type r4 = _StripRegex<'?/rest'>
+type r5 = _StripRegex<'*'>
+type r6 = _StripRegex<'-other-stuff'>
+type r7 = _StripRegex<'/edit'>
+
+export interface _ModifierExtracTResult<
+  M extends _ParamModifier | '',
+  Rest extends string
+> {
+  modifier: M
+  rest: Rest
+}
+
+export type _ExtractModifier<P extends string> =
+  P extends `${_ParamModifier}${infer Rest}`
+    ? P extends `${infer M}${Rest}`
+      ? M extends _ParamModifier
+        ? _ModifierExtracTResult<M, Rest>
+        : // impossible case
+          never
+      : // impossible case
+        never
+    : // No modifier present
+      _ModifierExtracTResult<'', P>
+
+type m1 = _ExtractModifier<''>
+type m2 = _ExtractModifier<'-rest'>
+type m3 = _ExtractModifier<'edit'>
+type m4 = _ExtractModifier<'+'>
+type m5 = _ExtractModifier<'+/edit'>
+
+export type _StripModifierAndRegex_OLD<S extends string> =
+  // do we have an escaped closing parenthesis?
+  S extends `${infer A}\\)${infer Rest}`
+    ? // the actual regexp finished before, A has no escaped )
+      A extends `${string})${infer Rest2}`
+      ? // get the modifier if there is one
+        Rest2 extends `${_ParamModifier}${infer Rest3}`
+        ? Rest2 extends `${infer M}${Rest3}`
+          ? { mod: M; rest: `${Rest3}\\)${Rest}` }
+          : never
+        : // No modifier
+          { mod: ''; rest: `${Rest2}\\)${Rest}` } // job done
+      : _RemoveUntilClosingPar<Rest> // we keep removing
+    : // simple case with no escaping
+    S extends `${string})${infer Rest}`
+    ? // extract the modifier if there is one
+      Rest extends `${_ParamModifier}${infer Rest2}`
+      ? Rest extends `${infer M}${Rest2}`
+        ? { mod: M; rest: Rest2 }
+        : never
+      : // no modifier
+        { mod: ''; rest: Rest }
+    : // nothing to remove
+      { mod: ''; rest: S }
+
 /**
  * Gets the possible type of a param based on its modifier M.
  *
@@ -158,8 +309,10 @@ export type _ParamToObject<
  * @internal
  */
 export type _RemoveUntilClosingPar<S extends string> =
+  // do we have an escaped closing parenthesis?
   S extends `${infer A}\\)${infer Rest}`
-    ? A extends `${string})${infer Rest2}` // the actual regexp finished before, AA has no escaped )
+    ? // the actual regexp finished before, A has no escaped )
+      A extends `${string})${infer Rest2}`
       ? Rest2 extends `${_ParamModifier}${infer Rest3}`
         ? Rest2 extends `${infer M}${Rest3}`
           ? `${M}}${Rest3}\\)${Rest}`
@@ -174,6 +327,9 @@ export type _RemoveUntilClosingPar<S extends string> =
       : `}${Rest}`
     : never // nothing to remove, should not have been called, easier to spot bugs
 
+type r = _RemoveUntilClosingPar<`aouest)/end`>
+type r2 = _RemoveUntilClosingPar<`aouest`>
+
 /**
  * Reformats a path string `/:id(custom-regex)/:other+` by wrapping params with
  * `{}` and removing custom regexps to make them easier to parse.
@@ -281,6 +437,7 @@ export type _BuildPath<
 
 /**
  * Builds a path string type from a path definition and an object of params.
+ *
  * @example
  * ```ts
  * type url = PathFromParams<'/users/:id', { id: 'posva' }> -> '/users/posva'
@@ -342,3 +499,69 @@ export type _JoinPath<
   : '' extends Prefix
   ? never
   : `${Prefix}${Prefix extends `${string}/` ? '' : '/'}${Path}`
+
+/**
+ * @internal
+ */
+type _AlphaNumeric =
+  | 'a'
+  | 'A'
+  | 'b'
+  | 'B'
+  | 'c'
+  | 'C'
+  | 'd'
+  | 'D'
+  | 'e'
+  | 'E'
+  | 'f'
+  | 'F'
+  | 'g'
+  | 'G'
+  | 'h'
+  | 'H'
+  | 'i'
+  | 'I'
+  | 'j'
+  | 'J'
+  | 'k'
+  | 'K'
+  | 'l'
+  | 'L'
+  | 'm'
+  | 'M'
+  | 'n'
+  | 'N'
+  | 'o'
+  | 'O'
+  | 'p'
+  | 'P'
+  | 'q'
+  | 'Q'
+  | 'r'
+  | 'R'
+  | 's'
+  | 'S'
+  | 't'
+  | 'T'
+  | 'u'
+  | 'U'
+  | 'v'
+  | 'V'
+  | 'w'
+  | 'W'
+  | 'x'
+  | 'X'
+  | 'y'
+  | 'Y'
+  | '0'
+  | '1'
+  | '2'
+  | '3'
+  | '4'
+  | '5'
+  | '6'
+  | '7'
+  | '8'
+  | '9'
+  | '_'
index 0ae4918bde8b790b084ebff7459cfc04933f33e2..7b6d6f6805fd4bdda88cba0db5a6594841d20e08 100644 (file)
@@ -64,7 +64,7 @@ expectType<{ date: string }>(params('/users/:date(\\d{4}-\\d{2}-\\d{2})'))
 expectType<{ a: string }>(params('/:a(pre-(?:\\d{0,5}\\)-end)'))
 
 // special characters
-expectType<{ id$thing: string }>(params('/:id$thing'))
+expectType<{ id: string }>(params('/:id$thing'))
 expectType<{ id: string }>(params('/:id&thing'))
 expectType<{ id: string }>(params('/:id!thing'))
 expectType<{ id: string }>(params('/:id\\*thing'))
diff --git a/packages/router/test-dts/perfNamedRoutes.test-d.ts b/packages/router/test-dts/perfNamedRoutes.test-d.ts
new file mode 100644 (file)
index 0000000..251417b
--- /dev/null
@@ -0,0 +1,155 @@
+import {
+  RouteRecordRaw,
+  RouteNamedMap,
+  RouteStaticPathMap,
+  RouteLocationNamedRaw,
+} from '.'
+import { defineComponent } from 'vue'
+
+const Home = defineComponent({})
+const User = defineComponent({})
+const LongView = defineComponent({})
+const component = defineComponent({})
+
+function defineRoutes<R extends Readonly<RouteRecordRaw[]>>(routes: R): R {
+  return routes
+}
+
+const routes = [
+  { path: '/home', redirect: '/' },
+  {
+    path: '/',
+    components: { default: Home, other: component },
+  },
+  {
+    path: '/always-redirect',
+    component,
+  },
+  { path: '/users/:id', name: 'user', component: User, props: true },
+  { path: '/documents/:id', name: 'docs', component: User, props: true },
+  { path: '/optional/:id?', name: 'optional', component: User, props: true },
+  { path: encodeURI('/n/€'), name: 'euro', component },
+  { path: '/n/:n', name: 'increment', component },
+  { path: '/multiple/:a/:b', name: 'multiple', component },
+  { path: '/long-:n', name: 'long', component: LongView },
+  {
+    path: '/lazy',
+    component,
+  },
+  {
+    path: '/with-guard/:n',
+    name: 'guarded',
+    component,
+  },
+  { path: '/cant-leave', component },
+  {
+    path: '/children',
+    name: 'WithChildren',
+    component,
+    children: [
+      { path: '', alias: 'alias', name: 'default-child', component },
+      { path: 'a', name: 'a-child', component },
+      {
+        path: 'b',
+        name: 'WithChildrenB',
+        component,
+        children: [
+          {
+            path: '',
+            name: 'b-child',
+            component,
+          },
+          { path: 'a2', component },
+          { path: 'b2', component },
+        ],
+      },
+    ],
+  },
+  { path: '/with-data', component, name: 'WithData' },
+  { path: '/rep/:a*', component, name: 'repeat' },
+  { path: '/:data(.*)', component, name: 'NotFound' },
+  {
+    path: '/nested',
+    alias: '/anidado',
+    component,
+    name: 'Nested',
+    children: [
+      {
+        path: 'nested',
+        alias: 'a',
+        name: 'NestedNested',
+        component,
+        children: [
+          {
+            name: 'NestedNestedNested',
+            path: 'nested',
+            component,
+          },
+        ],
+      },
+      {
+        path: 'other',
+        alias: 'otherAlias',
+        component,
+        name: 'NestedOther',
+      },
+      {
+        path: 'also-as-absolute',
+        alias: '/absolute',
+        name: 'absolute-child',
+        component,
+      },
+    ],
+  },
+
+  {
+    path: '/parent/:id',
+    name: 'parent',
+    component,
+    props: true,
+    alias: '/p/:id',
+    children: [
+      // empty child
+      { path: '', name: 'child-id', component },
+      // child with absolute path. we need to add an `id` because the parent needs it
+      { path: '/p_:id/absolute-a', alias: 'as-absolute-a', component },
+      // same as above but the alias is absolute
+      { path: 'as-absolute-b', alias: '/p_:id/absolute-b', component },
+    ],
+  },
+  {
+    path: '/dynamic',
+    name: 'dynamic',
+    component,
+    end: false,
+    strict: true,
+  },
+
+  {
+    path: '/admin',
+    children: [
+      { path: '', component },
+      { path: 'dashboard', component },
+      { path: 'settings', component },
+    ],
+  },
+] as const
+
+function pushStr(route: keyof RouteStaticPathMap<typeof routes>) {}
+pushStr('')
+
+// function push1(
+//   route: RouteNamedMap<typeof routes>[keyof RouteNamedMap<typeof routes>]
+// ) {}
+// push1({ })
+function pushEnd(route: keyof RouteNamedMap<typeof routes>) {}
+
+pushEnd('NotFound')
+
+function push(
+  route:
+    | keyof RouteStaticPathMap<typeof routes>
+    | {
+        name: keyof RouteNamedMap<typeof routes>
+      }
+) {}