From: Eduardo San Martin Morote Date: Mon, 13 Jun 2022 16:44:16 +0000 (+0200) Subject: refactor(types): simplify param parsing X-Git-Tag: v4.1.0~25 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=abff1a099d982700df7917a35eccdee4a2a91816;p=thirdparty%2Fvuejs%2Frouter.git refactor(types): simplify param parsing --- diff --git a/packages/playground/src/router.ts b/packages/playground/src/router.ts index c4c1b610..ca88c525 100644 --- a/packages/playground/src/router.ts +++ b/packages/playground/src/router.ts @@ -52,7 +52,7 @@ export const router = createRouter({ meta: { transition: 'slide-left' }, component: async () => { await delay(500) - return component() + return component }, }, { diff --git a/packages/router/src/types/index.ts b/packages/router/src/types/index.ts index 291c0b90..f265b2c2 100644 --- a/packages/router/src/types/index.ts +++ b/packages/router/src/types/index.ts @@ -151,10 +151,13 @@ export type RouteLocationNamedRaw< ? // allows assigning a RouteLocationRaw to RouteLocationNamedRaw RouteQueryAndHash & LocationAsRelativeRaw & RouteLocationOptions : { - [K in Extract]: RouteQueryAndHash & - LocationAsRelativeRaw & - RouteLocationOptions - }[Extract] + [K in Extract]: LocationAsRelativeRaw< + K, + RouteMap[K] + > + }[Extract] & + RouteQueryAndHash & + RouteLocationOptions /** * Route Location that can infer the possible paths. diff --git a/packages/router/src/types/paths.ts b/packages/router/src/types/paths.ts index e2318e34..25fe6cb0 100644 --- a/packages/router/src/types/paths.ts +++ b/packages/router/src/types/paths.ts @@ -8,23 +8,15 @@ import { RouteParams, RouteParamsRaw, RouteParamValueRaw } from '.' * type P = ParamsFromPath<'/:id/b/:c*'> // { id: string; c?: string[] } * ``` */ -export type ParamsFromPath

= string extends P - ? RouteParams // Generic version - : _ExtractParamsPath<_RemoveRegexpFromParam

, false> extends Record< - any, - never - > - ? Record - : _ExtractParamsPath<_RemoveRegexpFromParam

, false> - -export type ParamsRawFromPath

= string extends P - ? RouteParamsRaw // Generic version - : _ExtractParamsPath<_RemoveRegexpFromParam

, true> extends Record< - any, - never - > - ? Record - : _ExtractParamsPath<_RemoveRegexpFromParam

, true> +export type ParamsFromPath

= + P extends `${string}:${string}` + ? Simplify<_ExtractParamsOfPath> + : Record + +export type ParamsRawFromPath

= + P extends `${string}:${string}` + ? Simplify<_ExtractParamsOfPath> + : Record /** * Possible param modifiers. @@ -57,6 +49,7 @@ export type _ParamDelimiter = | '@' | '[' | ']' + | '$' | _ParamModifier /** @@ -78,6 +71,164 @@ export type _ExtractParamsPath< _ExtractParamsPath : {} +type _PathParam

= + | `${string}:${P}` + | `${string}:${P}${_ParamModifier}${Rest}` + +type b = '/' extends _PathParam ? P : never +type c = '/home' extends _PathParam ? P : never +type d = '/user/:id' extends _PathParam ? [P, Rest] : never +type e = '/user/:id+' extends _PathParam ? P : never + +export type _ExtractParamsOfPath< + P extends string, + isRaw extends boolean +> = P extends `${string}:${infer HasParam}` + ? _ParamName 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> extends _ModifierExtracTResult< + infer Modifier, + infer Rest2 + > + ? _ParamToObject & + _ExtractParamsOfPath + : { + NO: 1 // this should never happen as the modifier can be empty + } + : // Nothing after the param: /:id, we are done + _ParamToObject + : { + // 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 = { [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 `${_AlphaNumeric}${infer Rest}` + ? P extends `${infer C}${Rest}` + ? // Keep extracting other alphanumeric chars + `${C}${_ParamName_OLD}` + : 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

{ + 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 + : 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 + +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 = + // 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 // 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 `${_ParamModifier}${infer Rest}` + ? P extends `${infer M}${Rest}` + ? M extends _ParamModifier + ? _ModifierExtracTResult + : // 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 = + // 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 // 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 = + // 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 = : `}${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' + | '_' diff --git a/packages/router/test-dts/paths.test-d.ts b/packages/router/test-dts/paths.test-d.ts index 0ae4918b..7b6d6f68 100644 --- a/packages/router/test-dts/paths.test-d.ts +++ b/packages/router/test-dts/paths.test-d.ts @@ -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 index 00000000..251417b0 --- /dev/null +++ b/packages/router/test-dts/perfNamedRoutes.test-d.ts @@ -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>(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) {} +pushStr('') + +// function push1( +// route: RouteNamedMap[keyof RouteNamedMap] +// ) {} +// push1({ }) +function pushEnd(route: keyof RouteNamedMap) {} + +pushEnd('NotFound') + +function push( + route: + | keyof RouteStaticPathMap + | { + name: keyof RouteNamedMap + } +) {}