@esportsplus/routing 0.2.7 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,5 +12,5 @@ on:
12
12
  jobs:
13
13
  publish:
14
14
  secrets:
15
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN_PUBLISHING }}
16
- uses: esportsplus/workflows/.github/workflows/publish.yml@main
15
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
16
+ uses: esportsplus/workflows/.github/workflows/publish.yml@main
@@ -1,16 +1,17 @@
1
- import { Middleware, Next, Request, Route, Router } from './types.js';
1
+ import { Middleware, Next, PathParamsTuple, Request, Route, Router, RouteRegistry } from './types.js';
2
+ import { Router as RouterClass } from './router/index.js';
2
3
  declare function back(): void;
3
4
  declare function forward(): void;
4
- declare const _default: <T>(instance?: Router<T>) => {
5
+ declare const _default: <T, TRoutes extends RouteRegistry = {}>(instance?: RouterClass<T, TRoutes>) => {
5
6
  back: typeof back;
6
7
  forward: typeof forward;
7
8
  middleware: {
8
- (...middleware: Middleware<T>[]): T;
9
+ (...middleware: Middleware<T>[]): import("@esportsplus/pipeline").Pipeline<Request<T>, T>;
9
10
  dispatch(request: Request<T>): T;
10
11
  match(fallback: Route<T>): (request: Request<T>, next: Next<T>) => T;
11
12
  };
12
- redirect: (path: string, values?: unknown[]) => void;
13
- router: Router<T>;
14
- uri: (path: string, values?: unknown[]) => string;
13
+ redirect: <TName extends keyof TRoutes & string>(name: TName, values?: PathParamsTuple<TRoutes[TName]["path"]>) => void;
14
+ router: Router<T, TRoutes>;
15
+ uri: <TName extends keyof TRoutes & string>(name: TName, values?: PathParamsTuple<TRoutes[TName]["path"]>) => string;
15
16
  };
16
17
  export default _default;
package/build/browser.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { effect, reactive, root } from '@esportsplus/reactivity';
2
- import { next } from '@esportsplus/pipeline';
2
+ import pipeline from '@esportsplus/pipeline';
3
3
  import factory from './router/index.js';
4
4
  let cache = [], location = window.location;
5
5
  function back() {
@@ -10,8 +10,8 @@ function forward() {
10
10
  }
11
11
  function href() {
12
12
  let hash = location.hash || '#/', path = hash ? hash.slice(1).split('?') : ['/', ''], request = {
13
- href: location.href,
14
13
  hostname: location.hostname,
14
+ href: location.href,
15
15
  method: 'GET',
16
16
  origin: location.origin,
17
17
  path: path[0],
@@ -20,7 +20,7 @@ function href() {
20
20
  query: {}
21
21
  };
22
22
  if (path[1]) {
23
- let query = request.query, params = new URLSearchParams(path[1]);
23
+ let params = new URLSearchParams(path[1]), query = request.query;
24
24
  for (let [key, value] of params.entries()) {
25
25
  query[key] = value;
26
26
  }
@@ -42,7 +42,7 @@ function match(request, router, subdomain) {
42
42
  }
43
43
  function middleware(request, router) {
44
44
  function host(...middleware) {
45
- return middleware[0](request, next(1, middleware));
45
+ return pipeline(middleware);
46
46
  }
47
47
  ;
48
48
  host.dispatch = (request) => {
@@ -104,15 +104,15 @@ export default (instance) => {
104
104
  back,
105
105
  forward,
106
106
  middleware: middleware(request, router),
107
- redirect: (path, values = []) => {
108
- if (path.indexOf('://') !== -1) {
109
- return window.location.replace(path);
107
+ redirect: (name, values = []) => {
108
+ if (name.indexOf('://') !== -1) {
109
+ return window.location.replace(name);
110
110
  }
111
- window.location.hash = normalize(router.uri(path, values));
111
+ window.location.hash = normalize(router.uri(name, values));
112
112
  },
113
113
  router,
114
- uri: (path, values = []) => {
115
- return normalize(router.uri(path, values));
114
+ uri: (name, values = []) => {
115
+ return normalize(router.uri(name, values));
116
116
  }
117
117
  };
118
118
  };
@@ -1,7 +1,7 @@
1
- import { Name, Options, Route, RouteOptions } from '../types.js';
1
+ import { Name, Options, PathParamsTuple, Route, RouteOptions, RouteRegistry } from '../types.js';
2
2
  import { Node } from './node.js';
3
3
  declare function key(method: string, subdomain?: string | null): string;
4
- declare class Router<T> {
4
+ declare class Router<T, TRoutes extends RouteRegistry = {}> {
5
5
  bucket: Record<ReturnType<typeof key>, {
6
6
  root: Node<T>;
7
7
  static: Record<string, Route<T>>;
@@ -10,22 +10,57 @@ declare class Router<T> {
10
10
  routes: Record<Name, Route<T>>;
11
11
  subdomains: string[] | null;
12
12
  private add;
13
- private route;
14
- delete(options: RouteOptions<T>): this;
15
- get(options: RouteOptions<T>): this;
13
+ private create;
14
+ delete<TName extends string = string, TPath extends string = string>(options: RouteOptions<T> & {
15
+ name?: TName;
16
+ path?: TPath;
17
+ }): Router<T, TRoutes & (TName extends string ? TPath extends string ? {
18
+ [K in TName]: {
19
+ path: TPath;
20
+ };
21
+ } : TRoutes : TRoutes)>;
22
+ get<TName extends string = string, TPath extends string = string>(options: RouteOptions<T> & {
23
+ name?: TName;
24
+ path?: TPath;
25
+ }): Router<T, TRoutes & (TName extends string ? TPath extends string ? {
26
+ [K in TName]: {
27
+ path: TPath;
28
+ };
29
+ } : TRoutes : TRoutes)>;
16
30
  group(options: Options<T>): {
17
- routes: (fn: (router: Router<T>) => void) => void;
31
+ routes: (fn: (router: Router<T, TRoutes>) => void) => Router<T, TRoutes>;
18
32
  };
19
33
  match(method: string, path: string, subdomain?: string | null): {
20
34
  parameters?: Readonly<Record<PropertyKey, unknown>>;
21
35
  route?: Readonly<Route<T>> | undefined;
22
36
  };
23
- on(methods: string[], options: RouteOptions<T>): this;
24
- post(options: RouteOptions<T>): this;
25
- put(options: RouteOptions<T>): this;
26
- uri(name: Name, values?: unknown[]): string;
37
+ on<TName extends string = string, TPath extends string = string>(methods: string[], options: RouteOptions<T> & {
38
+ name?: TName;
39
+ path?: TPath;
40
+ }): Router<T, TRoutes & (TName extends string ? TPath extends string ? {
41
+ [K in TName]: {
42
+ path: TPath;
43
+ };
44
+ } : TRoutes : TRoutes)>;
45
+ post<TName extends string = string, TPath extends string = string>(options: RouteOptions<T> & {
46
+ name?: TName;
47
+ path?: TPath;
48
+ }): Router<T, TRoutes & (TName extends string ? TPath extends string ? {
49
+ [K in TName]: {
50
+ path: TPath;
51
+ };
52
+ } : TRoutes : TRoutes)>;
53
+ put<TName extends string = string, TPath extends string = string>(options: RouteOptions<T> & {
54
+ name?: TName;
55
+ path?: TPath;
56
+ }): Router<T, TRoutes & (TName extends string ? TPath extends string ? {
57
+ [K in TName]: {
58
+ path: TPath;
59
+ };
60
+ } : TRoutes : TRoutes)>;
61
+ uri<TName extends keyof TRoutes & string>(name: TName, values?: PathParamsTuple<TRoutes[TName]['path']>): string;
27
62
  }
28
- declare const _default: <T>() => Router<T>;
63
+ declare const _default: <T>() => Router<T, {}>;
29
64
  export default _default;
30
65
  export { Router };
31
66
  export type { Route };
@@ -53,7 +53,7 @@ class Router {
53
53
  }
54
54
  return this;
55
55
  }
56
- route(options) {
56
+ create(options) {
57
57
  let groups = this.groups, route = {
58
58
  name: null,
59
59
  path: null,
@@ -73,10 +73,12 @@ class Router {
73
73
  return route;
74
74
  }
75
75
  delete(options) {
76
- return this.on(ON_DELETE, options);
76
+ this.on(ON_DELETE, options);
77
+ return this;
77
78
  }
78
79
  get(options) {
79
- return this.on(ON_GET, options);
80
+ this.on(ON_GET, options);
81
+ return this;
80
82
  }
81
83
  group(options) {
82
84
  return {
@@ -84,6 +86,7 @@ class Router {
84
86
  this.groups.push(options);
85
87
  fn(this);
86
88
  this.groups.pop();
89
+ return this;
87
90
  }
88
91
  };
89
92
  }
@@ -99,7 +102,7 @@ class Router {
99
102
  return bucket.root.find(path);
100
103
  }
101
104
  on(methods, options) {
102
- let route = this.route(options);
105
+ let route = this.create(options);
103
106
  let name = route.name, path = route.path, subdomain = route.subdomain;
104
107
  if (name) {
105
108
  if (this.routes[name]) {
@@ -129,10 +132,12 @@ class Router {
129
132
  return this;
130
133
  }
131
134
  post(options) {
132
- return this.on(ON_POST, options);
135
+ this.on(ON_POST, options);
136
+ return this;
133
137
  }
134
138
  put(options) {
135
- return this.on(ON_PUT, options);
139
+ this.on(ON_PUT, options);
140
+ return this;
136
141
  }
137
142
  uri(name, values = []) {
138
143
  let path = this.routes[name]?.path;
package/build/types.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  import { NeverAsync } from '@esportsplus/utilities';
2
2
  import { Router } from './router/index.js';
3
3
  import pipeline from '@esportsplus/pipeline';
4
+ type ExtractOptionalParamsTuple<Path extends string> = Path extends `${infer _Start}?:${infer Param}/${infer Rest}` ? [Param, ...ExtractOptionalParamsTuple<`/${Rest}`>] : Path extends `${infer _Start}?:${infer Param}` ? [Param] : [];
5
+ type ExtractParamsTuple<Path extends string> = Path extends `${infer _Start}:${infer Param}/${infer Rest}` ? [Param, ...ExtractParamsTuple<`/${Rest}`>] : Path extends `${infer _Start}:${infer Param}` ? [Param] : [];
6
+ type ExtractWildcard<Path extends string> = Path extends `${string}*:${infer Param}` ? Param : never;
7
+ type LabeledParamsTuple<Params extends string[]> = Params extends [infer _First extends string, ...infer Rest extends string[]] ? [_First: string | number, ...LabeledParamsTuple<Rest>] : [];
4
8
  type Middleware<T> = NeverAsync<(input: Request<T>, next: Next<T>) => T>;
5
9
  type Name = string;
6
10
  type Next<T> = NeverAsync<(input: Request<T>) => T>;
@@ -10,6 +14,11 @@ type Options<T> = {
10
14
  path?: string;
11
15
  subdomain?: string;
12
16
  };
17
+ type PathParamsTuple<Path extends string> = LabeledParamsTuple<ExtractParamsTuple<Path>>;
18
+ type PathParamsTupleWithOptional<Path extends string> = [
19
+ ...LabeledParamsTuple<ExtractParamsTuple<Path>>,
20
+ ...Partial<LabeledParamsTuple<ExtractOptionalParamsTuple<Path>>>
21
+ ];
13
22
  type Request<T> = {
14
23
  data: Record<PropertyKey, unknown> & ReturnType<Router<T>['match']>;
15
24
  href: string;
@@ -31,4 +40,7 @@ type Route<T> = {
31
40
  type RouteOptions<T> = Options<T> & {
32
41
  responder: Next<T>;
33
42
  };
34
- export type { Middleware, Name, Next, Options, Request, Route, RouteOptions, Router };
43
+ type RouteRegistry = Record<string, {
44
+ path: string;
45
+ }>;
46
+ export type { ExtractOptionalParamsTuple, ExtractParamsTuple, ExtractWildcard, LabeledParamsTuple, Middleware, Name, Next, Options, PathParamsTuple, PathParamsTupleWithOptional, Request, Route, RouteOptions, Router, RouteRegistry };
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "author": "ICJR",
3
3
  "dependencies": {
4
- "@esportsplus/pipeline": "^1.1.5",
5
- "@esportsplus/reactivity": "^0.18.0",
6
- "@esportsplus/utilities": "^0.22.2"
4
+ "@esportsplus/pipeline": "^1.2.0",
5
+ "@esportsplus/reactivity": "^0.22.1",
6
+ "@esportsplus/utilities": "^0.26.0"
7
7
  },
8
8
  "devDependencies": {
9
9
  "@esportsplus/typescript": "^0.9.2"
@@ -11,10 +11,14 @@
11
11
  "main": "./build/index.js",
12
12
  "name": "@esportsplus/routing",
13
13
  "private": false,
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/esportsplus/routing"
17
+ },
14
18
  "type": "module",
15
19
  "sideEffects": false,
16
20
  "types": "./build/index.d.ts",
17
- "version": "0.2.7",
21
+ "version": "0.3.1",
18
22
  "scripts": {
19
23
  "build": "tsc && tsc-alias",
20
24
  "-": "-",
package/src/browser.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { effect, reactive, root } from '@esportsplus/reactivity';
2
- import { Middleware, Next, Request, Route, Router } from './types';
3
- import { next } from '@esportsplus/pipeline';
2
+ import pipeline from '@esportsplus/pipeline';
3
+ import { Middleware, Next, PathParamsTuple, Request, Route, Router, RouteRegistry } from './types';
4
+ import { Router as RouterClass } from './router';
4
5
  import factory from './router';
5
6
 
6
7
 
@@ -20,8 +21,8 @@ function href<T>() {
20
21
  let hash = location.hash || '#/',
21
22
  path = hash ? hash.slice(1).split('?') : ['/', ''],
22
23
  request = {
23
- href: location.href,
24
24
  hostname: location.hostname,
25
+ href: location.href,
25
26
  method: 'GET',
26
27
  origin: location.origin,
27
28
  path: path[0],
@@ -31,8 +32,8 @@ function href<T>() {
31
32
  };
32
33
 
33
34
  if (path[1]) {
34
- let query = request.query,
35
- params = new URLSearchParams(path[1]);
35
+ let params = new URLSearchParams(path[1]),
36
+ query = request.query;
36
37
 
37
38
  for (let [key, value] of params.entries()) {
38
39
  query[key] = value;
@@ -62,7 +63,7 @@ function match<T>(request: Request<T>, router: Router<T>, subdomain?: string) {
62
63
 
63
64
  function middleware<T>(request: Request<T>, router: Router<T>) {
64
65
  function host(...middleware: Middleware<T>[]) {
65
- return middleware[0](request, next(1, middleware));
66
+ return pipeline(middleware);
66
67
  };
67
68
 
68
69
  host.dispatch = (request: Request<T>) => {
@@ -133,9 +134,9 @@ function onpopstate() {
133
134
  }
134
135
 
135
136
 
136
- export default <T>(instance?: Router<T>) => {
137
+ export default <T, TRoutes extends RouteRegistry = {}>(instance?: RouterClass<T, TRoutes>) => {
137
138
  let request = reactive( Object.assign(href<T>(), { data: {} } as any) as Request<T> ),
138
- router = instance || factory<T>();
139
+ router = instance || factory<T>() as RouterClass<T, TRoutes>;
139
140
 
140
141
  if (cache.push(request) === 1) {
141
142
  window.addEventListener('hashchange', onpopstate);
@@ -144,17 +145,23 @@ export default <T>(instance?: Router<T>) => {
144
145
  return {
145
146
  back,
146
147
  forward,
147
- middleware: middleware(request, router),
148
- redirect: (path: string, values: unknown[] = []) => {
149
- if (path.indexOf('://') !== -1) {
150
- return window.location.replace(path);
148
+ middleware: middleware(request, router as Router<T>),
149
+ redirect: <TName extends keyof TRoutes & string>(
150
+ name: TName,
151
+ values: PathParamsTuple<TRoutes[TName]['path']> = [] as any
152
+ ) => {
153
+ if ((name as string).indexOf('://') !== -1) {
154
+ return window.location.replace(name);
151
155
  }
152
156
 
153
- window.location.hash = normalize( router.uri(path, values) );
157
+ window.location.hash = normalize( router.uri(name, values) );
154
158
  },
155
159
  router,
156
- uri: (path: string, values: unknown[] = []) => {
157
- return normalize( router.uri(path, values) );
160
+ uri: <TName extends keyof TRoutes & string>(
161
+ name: TName,
162
+ values: PathParamsTuple<TRoutes[TName]['path']> = [] as any
163
+ ) => {
164
+ return normalize( router.uri(name, values) );
158
165
  }
159
166
  };
160
167
  };
@@ -1,5 +1,5 @@
1
1
  import { ON_DELETE, ON_GET, ON_POST, ON_PUT } from '../constants';
2
- import { Name, Options, Request, Route, RouteOptions } from '../types';
2
+ import { Name, Options, PathParamsTuple, Request, Route, RouteOptions, RouteRegistry } from '../types';
3
3
  import { Node } from './node';
4
4
  import pipeline from '@esportsplus/pipeline';
5
5
 
@@ -44,7 +44,7 @@ function set<T>(route: Route<T>, options: Options<T> | RouteOptions<T>) {
44
44
  }
45
45
 
46
46
 
47
- class Router<T> {
47
+ class Router<T, TRoutes extends RouteRegistry = {}> {
48
48
  bucket: Record<ReturnType<typeof key>, { root: Node<T>, static: Record<string, Route<T>> }> = {};
49
49
  groups: Options<T>[] = [];
50
50
  routes: Record<Name, Route<T>> = {};
@@ -71,7 +71,7 @@ class Router<T> {
71
71
  return this;
72
72
  }
73
73
 
74
- private route(options: RouteOptions<T>) {
74
+ private create(options: RouteOptions<T>) {
75
75
  let groups = this.groups,
76
76
  route: Route<T> = {
77
77
  name: null,
@@ -98,22 +98,31 @@ class Router<T> {
98
98
  }
99
99
 
100
100
 
101
- delete(options: RouteOptions<T>) {
102
- return this.on(ON_DELETE, options);
101
+ delete<TName extends string = string, TPath extends string = string>(
102
+ options: RouteOptions<T> & { name?: TName; path?: TPath }
103
+ ): Router<T, TRoutes & (TName extends string ? TPath extends string ? { [K in TName]: { path: TPath } } : TRoutes : TRoutes)> {
104
+ this.on(ON_DELETE, options);
105
+ return this as any;
103
106
  }
104
107
 
105
- get(options: RouteOptions<T>) {
106
- return this.on(ON_GET, options);
108
+ get<TName extends string = string, TPath extends string = string>(
109
+ options: RouteOptions<T> & { name?: TName; path?: TPath }
110
+ ): Router<T, TRoutes & (TName extends string ? TPath extends string ? { [K in TName]: { path: TPath } } : TRoutes : TRoutes)> {
111
+ this.on(ON_GET, options);
112
+ return this as any;
107
113
  }
108
114
 
109
- group(options: Options<T>) {
115
+ group(options: Options<T>): {
116
+ routes: (fn: (router: Router<T, TRoutes>) => void) => Router<T, TRoutes>
117
+ } {
110
118
  return {
111
- routes: (fn: (router: Router<T>) => void) => {
119
+ routes: (fn: (router: Router<T, TRoutes>) => void) => {
112
120
  this.groups.push(options);
113
121
  fn(this);
114
122
  this.groups.pop();
123
+ return this;
115
124
  }
116
- }
125
+ };
117
126
  }
118
127
 
119
128
  match(method: string, path: string, subdomain?: string | null) {
@@ -132,8 +141,11 @@ class Router<T> {
132
141
  return bucket.root.find(path);
133
142
  }
134
143
 
135
- on(methods: string[], options: RouteOptions<T>) {
136
- let route = this.route(options);
144
+ on<TName extends string = string, TPath extends string = string>(
145
+ methods: string[],
146
+ options: RouteOptions<T> & { name?: TName; path?: TPath }
147
+ ): Router<T, TRoutes & (TName extends string ? TPath extends string ? { [K in TName]: { path: TPath } } : TRoutes : TRoutes)> {
148
+ let route = this.create(options);
137
149
 
138
150
  let name = route.name,
139
151
  path = route.path,
@@ -172,25 +184,34 @@ class Router<T> {
172
184
  (this.subdomains ??= []).push( subdomain.toLowerCase() );
173
185
  }
174
186
 
175
- return this;
187
+ return this as any;
176
188
  }
177
189
 
178
- post(options: RouteOptions<T>) {
179
- return this.on(ON_POST, options);
190
+ post<TName extends string = string, TPath extends string = string>(
191
+ options: RouteOptions<T> & { name?: TName; path?: TPath }
192
+ ): Router<T, TRoutes & (TName extends string ? TPath extends string ? { [K in TName]: { path: TPath } } : TRoutes : TRoutes)> {
193
+ this.on(ON_POST, options);
194
+ return this as any;
180
195
  }
181
196
 
182
- put(options: RouteOptions<T>) {
183
- return this.on(ON_PUT, options);
197
+ put<TName extends string = string, TPath extends string = string>(
198
+ options: RouteOptions<T> & { name?: TName; path?: TPath }
199
+ ): Router<T, TRoutes & (TName extends string ? TPath extends string ? { [K in TName]: { path: TPath } } : TRoutes : TRoutes)> {
200
+ this.on(ON_PUT, options);
201
+ return this as any;
184
202
  }
185
203
 
186
- uri(name: Name, values: unknown[] = []) {
204
+ uri<TName extends keyof TRoutes & string>(
205
+ name: TName,
206
+ values: PathParamsTuple<TRoutes[TName]['path']> = [] as any
207
+ ): string {
187
208
  let path = this.routes[name]?.path;
188
209
 
189
210
  if (!path) {
190
211
  throw new Error(`Routing: route name '${name}' does not exist or it does not provide a path`);
191
212
  }
192
213
 
193
- let resolved = [] as typeof values,
214
+ let resolved: (string | number)[] = [],
194
215
  segments = path.split('/'),
195
216
  v = 0;
196
217
 
@@ -199,18 +220,18 @@ class Router<T> {
199
220
  symbol = segment[0];
200
221
 
201
222
  if (symbol === ':') {
202
- resolved.push(values[v++]);
223
+ resolved.push((values as (string | number)[])[v++]);
203
224
  }
204
225
  else if (symbol === '?') {
205
- if (values[v] === undefined) {
226
+ if ((values as (string | number)[])[v] === undefined) {
206
227
  break;
207
228
  }
208
229
 
209
- resolved.push(values[v++]);
230
+ resolved.push((values as (string | number)[])[v++]);
210
231
  }
211
232
  else if (symbol === '*') {
212
233
  for (let n = values.length; v < n; v++) {
213
- resolved.push( values[v] );
234
+ resolved.push((values as (string | number)[])[v]);
214
235
  }
215
236
  break;
216
237
  }
@@ -224,6 +245,6 @@ class Router<T> {
224
245
  }
225
246
 
226
247
 
227
- export default <T>() => new Router<T>();
248
+ export default <T>() => new Router<T, {}>();
228
249
  export { Router };
229
250
  export type { Route };
package/src/types.ts CHANGED
@@ -3,6 +3,30 @@ import { Router } from './router';
3
3
  import pipeline from '@esportsplus/pipeline';
4
4
 
5
5
 
6
+ type ExtractOptionalParamsTuple<Path extends string> =
7
+ Path extends `${infer _Start}?:${infer Param}/${infer Rest}`
8
+ ? [Param, ...ExtractOptionalParamsTuple<`/${Rest}`>]
9
+ : Path extends `${infer _Start}?:${infer Param}`
10
+ ? [Param]
11
+ : [];
12
+
13
+ type ExtractParamsTuple<Path extends string> =
14
+ Path extends `${infer _Start}:${infer Param}/${infer Rest}`
15
+ ? [Param, ...ExtractParamsTuple<`/${Rest}`>]
16
+ : Path extends `${infer _Start}:${infer Param}`
17
+ ? [Param]
18
+ : [];
19
+
20
+ type ExtractWildcard<Path extends string> =
21
+ Path extends `${string}*:${infer Param}`
22
+ ? Param
23
+ : never;
24
+
25
+ type LabeledParamsTuple<Params extends string[]> =
26
+ Params extends [infer _First extends string, ...infer Rest extends string[]]
27
+ ? [_First: string | number, ...LabeledParamsTuple<Rest>]
28
+ : [];
29
+
6
30
  type Middleware<T> = NeverAsync<(input: Request<T>, next: Next<T>) => T>;
7
31
 
8
32
  type Name = string;
@@ -16,6 +40,12 @@ type Options<T> = {
16
40
  subdomain?: string;
17
41
  };
18
42
 
43
+ type PathParamsTuple<Path extends string> =
44
+ LabeledParamsTuple<ExtractParamsTuple<Path>>;
45
+
46
+ type PathParamsTupleWithOptional<Path extends string> =
47
+ [...LabeledParamsTuple<ExtractParamsTuple<Path>>, ...Partial<LabeledParamsTuple<ExtractOptionalParamsTuple<Path>>>];
48
+
19
49
  type Request<T> = {
20
50
  data: Record<PropertyKey, unknown> & ReturnType<Router<T>['match']>;
21
51
  href: string;
@@ -40,10 +70,23 @@ type RouteOptions<T> = Options<T> & {
40
70
  responder: Next<T>;
41
71
  };
42
72
 
73
+ type RouteRegistry = Record<string, { path: string }>;
74
+
43
75
 
44
76
  export type {
77
+ ExtractOptionalParamsTuple,
78
+ ExtractParamsTuple,
79
+ ExtractWildcard,
80
+ LabeledParamsTuple,
45
81
  Middleware,
46
- Name, Next,
82
+ Name,
83
+ Next,
47
84
  Options,
48
- Request, Route, RouteOptions, Router
85
+ PathParamsTuple,
86
+ PathParamsTupleWithOptional,
87
+ Request,
88
+ Route,
89
+ RouteOptions,
90
+ Router,
91
+ RouteRegistry
49
92
  };