@b9g/router 0.1.10 → 0.2.0-beta.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.
package/src/index.js CHANGED
@@ -6,15 +6,17 @@ import {
6
6
  compilePathname
7
7
  } from "@b9g/match-pattern";
8
8
  import {
9
+ isHTTPError,
9
10
  InternalServerError,
10
- NotFound,
11
- isHTTPError
11
+ NotFound
12
12
  } from "@b9g/http-errors";
13
+ import { getLogger } from "@logtape/logtape";
14
+ var logger = getLogger(["shovel", "router"]);
13
15
  var RadixNode = class {
14
16
  children;
15
17
  // char -> RadixNode
16
- handlers;
17
- // method -> handler
18
+ routes;
19
+ // method -> RouteEntry
18
20
  paramName;
19
21
  // param name if this is a :param segment
20
22
  paramChild;
@@ -23,7 +25,7 @@ var RadixNode = class {
23
25
  // child node for * wildcard
24
26
  constructor() {
25
27
  this.children = /* @__PURE__ */ new Map();
26
- this.handlers = /* @__PURE__ */ new Map();
28
+ this.routes = /* @__PURE__ */ new Map();
27
29
  this.paramName = null;
28
30
  this.paramChild = null;
29
31
  this.wildcardChild = null;
@@ -38,22 +40,17 @@ var RadixTreeExecutor = class {
38
40
  for (const route of routes) {
39
41
  const pathname = route.pattern.pathname;
40
42
  if (isSimplePattern(pathname)) {
41
- this.#addToTree(pathname, route.method, route.handler);
43
+ this.#addToTree(pathname, route);
42
44
  } else {
43
45
  const compiled = compilePathname(pathname);
44
- this.#complexRoutes.push({
45
- compiled,
46
- method: route.method,
47
- handler: route.handler,
48
- pattern: route.pattern
49
- });
46
+ this.#complexRoutes.push({ compiled, route });
50
47
  }
51
48
  }
52
49
  }
53
50
  /**
54
51
  * Add a simple pattern to the radix tree
55
52
  */
56
- #addToTree(pathname, method, handler) {
53
+ #addToTree(pathname, route) {
57
54
  let node = this.#root;
58
55
  let i = 0;
59
56
  while (i < pathname.length) {
@@ -84,15 +81,18 @@ var RadixTreeExecutor = class {
84
81
  node = node.children.get(char);
85
82
  i++;
86
83
  }
87
- node.handlers.set(method, handler);
84
+ node.routes.set(route.method, route);
88
85
  }
89
86
  /**
90
- * Match a pathname against the radix tree
87
+ * Match a pathname against the radix tree (for URL matching)
91
88
  */
92
- #matchTree(pathname, method) {
89
+ #matchTreeByPath(pathname) {
93
90
  const params = {};
94
91
  let node = this.#root;
95
92
  let i = 0;
93
+ if (!pathname) {
94
+ return node.routes.size > 0 ? { node, params } : null;
95
+ }
96
96
  while (i < pathname.length) {
97
97
  const char = pathname[i];
98
98
  if (node.children.has(char)) {
@@ -121,55 +121,101 @@ var RadixTreeExecutor = class {
121
121
  }
122
122
  return null;
123
123
  }
124
- let handler = node.handlers.get(method);
125
- if (!handler && method === "HEAD") {
126
- handler = node.handlers.get("GET");
124
+ if (node.routes.size > 0) {
125
+ return { node, params };
127
126
  }
128
- if (handler) {
129
- return { handler, params };
127
+ if (node.wildcardChild && node.wildcardChild.routes.size > 0) {
128
+ params["0"] = "";
129
+ return { node: node.wildcardChild, params };
130
130
  }
131
- if (node.wildcardChild) {
132
- let wildcardHandler = node.wildcardChild.handlers.get(method);
133
- if (!wildcardHandler && method === "HEAD") {
134
- wildcardHandler = node.wildcardChild.handlers.get("GET");
135
- }
136
- if (wildcardHandler) {
137
- params["0"] = "";
138
- return { handler: wildcardHandler, params };
131
+ return null;
132
+ }
133
+ /**
134
+ * Match a pathname against the radix tree (for request handling with method)
135
+ */
136
+ #matchTree(pathname, method) {
137
+ const result = this.#matchTreeByPath(pathname);
138
+ if (!result) return null;
139
+ const { node, params } = result;
140
+ let entry = node.routes.get(method);
141
+ if (!entry && method === "HEAD") {
142
+ entry = node.routes.get("GET");
143
+ }
144
+ if (entry) {
145
+ return { entry, params };
146
+ }
147
+ return null;
148
+ }
149
+ /**
150
+ * Match a URL against registered routes (returns RouteMatch info)
151
+ */
152
+ matchURL(url) {
153
+ const urlObj = typeof url === "string" ? new URL(url, "http://localhost") : url;
154
+ const pathname = urlObj.pathname;
155
+ const treeResult = this.#matchTreeByPath(pathname);
156
+ if (treeResult) {
157
+ const { node, params } = treeResult;
158
+ const methods = Array.from(node.routes.keys());
159
+ const firstEntry = node.routes.values().next().value;
160
+ return {
161
+ params,
162
+ methods,
163
+ name: firstEntry?.name,
164
+ pattern: firstEntry?.pattern.pathname ?? ""
165
+ };
166
+ }
167
+ for (const { compiled, route } of this.#complexRoutes) {
168
+ const match = pathname.match(compiled.regex);
169
+ if (match) {
170
+ const params = {};
171
+ for (let i = 0; i < compiled.paramNames.length; i++) {
172
+ if (match[i + 1] !== void 0) {
173
+ params[compiled.paramNames[i]] = match[i + 1];
174
+ }
175
+ }
176
+ const methods = this.#complexRoutes.filter((r) => r.route.pattern.pathname === route.pattern.pathname).map((r) => r.route.method);
177
+ return {
178
+ params,
179
+ methods,
180
+ name: route.name,
181
+ pattern: route.pattern.pathname
182
+ };
139
183
  }
140
184
  }
141
185
  return null;
142
186
  }
143
187
  /**
144
- * Find the first route that matches the request
188
+ * Find the first route that matches the request (for handling)
145
189
  */
146
- match(request) {
190
+ matchRequest(request) {
147
191
  const url = new URL(request.url);
148
192
  const method = request.method.toUpperCase();
149
193
  const pathname = url.pathname;
150
194
  const treeResult = this.#matchTree(pathname, method);
151
195
  if (treeResult) {
152
196
  return {
153
- handler: treeResult.handler,
154
- context: { params: treeResult.params }
197
+ handler: treeResult.entry.handler,
198
+ context: { params: treeResult.params },
199
+ entry: treeResult.entry
155
200
  };
156
201
  }
157
- for (const route of this.#complexRoutes) {
202
+ for (const { compiled, route } of this.#complexRoutes) {
158
203
  const methodMatches = route.method === method || method === "HEAD" && route.method === "GET";
159
204
  if (!methodMatches) {
160
205
  continue;
161
206
  }
162
- const match = pathname.match(route.compiled.regex);
207
+ const match = pathname.match(compiled.regex);
163
208
  if (match) {
164
209
  const params = {};
165
- for (let i = 0; i < route.compiled.paramNames.length; i++) {
210
+ for (let i = 0; i < compiled.paramNames.length; i++) {
166
211
  if (match[i + 1] !== void 0) {
167
- params[route.compiled.paramNames[i]] = match[i + 1];
212
+ params[compiled.paramNames[i]] = match[i + 1];
168
213
  }
169
214
  }
170
215
  return {
171
216
  handler: route.handler,
172
- context: { params }
217
+ context: { params },
218
+ entry: route
173
219
  };
174
220
  }
175
221
  }
@@ -179,57 +225,110 @@ var RadixTreeExecutor = class {
179
225
  var RouteBuilder = class {
180
226
  #router;
181
227
  #pattern;
182
- constructor(router, pattern) {
228
+ #name;
229
+ #middlewares;
230
+ constructor(router, pattern, options) {
183
231
  this.#router = router;
184
232
  this.#pattern = pattern;
233
+ this.#name = options?.name;
234
+ this.#middlewares = [];
235
+ }
236
+ /**
237
+ * Add route-scoped middleware that only runs when this pattern matches
238
+ */
239
+ use(middleware) {
240
+ this.#middlewares.push(middleware);
241
+ return this;
185
242
  }
186
243
  /**
187
244
  * Register a GET handler for this route pattern
188
245
  */
189
246
  get(handler) {
190
- this.#router.addRoute("GET", this.#pattern, handler);
247
+ this.#router.addRoute(
248
+ "GET",
249
+ this.#pattern,
250
+ handler,
251
+ this.#name,
252
+ this.#middlewares
253
+ );
191
254
  return this;
192
255
  }
193
256
  /**
194
257
  * Register a POST handler for this route pattern
195
258
  */
196
259
  post(handler) {
197
- this.#router.addRoute("POST", this.#pattern, handler);
260
+ this.#router.addRoute(
261
+ "POST",
262
+ this.#pattern,
263
+ handler,
264
+ this.#name,
265
+ this.#middlewares
266
+ );
198
267
  return this;
199
268
  }
200
269
  /**
201
270
  * Register a PUT handler for this route pattern
202
271
  */
203
272
  put(handler) {
204
- this.#router.addRoute("PUT", this.#pattern, handler);
273
+ this.#router.addRoute(
274
+ "PUT",
275
+ this.#pattern,
276
+ handler,
277
+ this.#name,
278
+ this.#middlewares
279
+ );
205
280
  return this;
206
281
  }
207
282
  /**
208
283
  * Register a DELETE handler for this route pattern
209
284
  */
210
285
  delete(handler) {
211
- this.#router.addRoute("DELETE", this.#pattern, handler);
286
+ this.#router.addRoute(
287
+ "DELETE",
288
+ this.#pattern,
289
+ handler,
290
+ this.#name,
291
+ this.#middlewares
292
+ );
212
293
  return this;
213
294
  }
214
295
  /**
215
296
  * Register a PATCH handler for this route pattern
216
297
  */
217
298
  patch(handler) {
218
- this.#router.addRoute("PATCH", this.#pattern, handler);
299
+ this.#router.addRoute(
300
+ "PATCH",
301
+ this.#pattern,
302
+ handler,
303
+ this.#name,
304
+ this.#middlewares
305
+ );
219
306
  return this;
220
307
  }
221
308
  /**
222
309
  * Register a HEAD handler for this route pattern
223
310
  */
224
311
  head(handler) {
225
- this.#router.addRoute("HEAD", this.#pattern, handler);
312
+ this.#router.addRoute(
313
+ "HEAD",
314
+ this.#pattern,
315
+ handler,
316
+ this.#name,
317
+ this.#middlewares
318
+ );
226
319
  return this;
227
320
  }
228
321
  /**
229
322
  * Register an OPTIONS handler for this route pattern
230
323
  */
231
324
  options(handler) {
232
- this.#router.addRoute("OPTIONS", this.#pattern, handler);
325
+ this.#router.addRoute(
326
+ "OPTIONS",
327
+ this.#pattern,
328
+ handler,
329
+ this.#name,
330
+ this.#middlewares
331
+ );
233
332
  return this;
234
333
  }
235
334
  /**
@@ -246,51 +345,34 @@ var RouteBuilder = class {
246
345
  "OPTIONS"
247
346
  ];
248
347
  methods.forEach((method) => {
249
- this.#router.addRoute(method, this.#pattern, handler);
348
+ this.#router.addRoute(
349
+ method,
350
+ this.#pattern,
351
+ handler,
352
+ this.#name,
353
+ this.#middlewares
354
+ );
250
355
  });
251
356
  return this;
252
357
  }
253
358
  };
254
359
  var Router = class {
255
- #routes;
256
- #middlewares;
360
+ routes;
361
+ middlewares;
257
362
  #executor;
258
- #dirty;
259
363
  constructor() {
260
- this.#routes = [];
261
- this.#middlewares = [];
364
+ this.routes = [];
365
+ this.middlewares = [];
262
366
  this.#executor = null;
263
- this.#dirty = false;
264
- this.#handlerImpl = async (request) => {
265
- try {
266
- if (this.#dirty || !this.#executor) {
267
- this.#executor = new RadixTreeExecutor(this.#routes);
268
- this.#dirty = false;
269
- }
270
- const matchResult = this.#executor.match(request);
271
- if (matchResult) {
272
- return await this.#executeMiddlewareStack(
273
- this.#middlewares,
274
- request,
275
- matchResult.context,
276
- matchResult.handler
277
- );
278
- } else {
279
- const notFoundHandler = async () => {
280
- throw new NotFound();
281
- };
282
- return await this.#executeMiddlewareStack(
283
- this.#middlewares,
284
- request,
285
- { params: {} },
286
- notFoundHandler
287
- );
288
- }
289
- } catch (error) {
290
- return this.#createErrorResponse(error);
291
- }
292
- };
293
- this.handler = this.#handlerImpl;
367
+ }
368
+ /**
369
+ * Ensure the executor is compiled and up to date
370
+ */
371
+ #ensureCompiled() {
372
+ if (!this.#executor) {
373
+ this.#executor = new RadixTreeExecutor(this.routes);
374
+ }
375
+ return this.#executor;
294
376
  }
295
377
  use(pathPrefixOrMiddleware, maybeMiddleware) {
296
378
  if (typeof pathPrefixOrMiddleware === "string") {
@@ -300,7 +382,7 @@ var Router = class {
300
382
  "Invalid middleware type. Must be function or async generator function."
301
383
  );
302
384
  }
303
- this.#middlewares.push({
385
+ this.middlewares.push({
304
386
  middleware,
305
387
  pathPrefix: pathPrefixOrMiddleware
306
388
  });
@@ -310,95 +392,89 @@ var Router = class {
310
392
  "Invalid middleware type. Must be function or async generator function."
311
393
  );
312
394
  }
313
- this.#middlewares.push({ middleware: pathPrefixOrMiddleware });
395
+ this.middlewares.push({ middleware: pathPrefixOrMiddleware });
314
396
  }
315
- this.#dirty = true;
397
+ this.#executor = null;
316
398
  }
317
- route(patternOrConfig) {
318
- if (typeof patternOrConfig === "string") {
319
- return new RouteBuilder(this, patternOrConfig);
320
- } else {
321
- return new RouteBuilder(this, patternOrConfig.pattern);
322
- }
399
+ /**
400
+ * Create a route builder for the given pattern
401
+ * Returns a chainable interface for registering HTTP method handlers
402
+ *
403
+ * Example:
404
+ * router.route('/api/users/:id', { name: 'user' })
405
+ * .use(authMiddleware)
406
+ * .get(getUserHandler)
407
+ * .put(updateUserHandler);
408
+ */
409
+ route(pattern, options) {
410
+ return new RouteBuilder(this, pattern, options);
323
411
  }
324
412
  /**
325
413
  * Internal method called by RouteBuilder to register routes
326
414
  * Public for RouteBuilder access, but not intended for direct use
327
415
  */
328
- addRoute(method, pattern, handler) {
416
+ addRoute(method, pattern, handler, name, middlewares = []) {
329
417
  const matchPattern = new MatchPattern(pattern);
330
- this.#routes.push({
418
+ this.routes.push({
331
419
  pattern: matchPattern,
332
420
  method: method.toUpperCase(),
333
- handler
421
+ handler,
422
+ name,
423
+ middlewares
334
424
  });
335
- this.#dirty = true;
425
+ this.#executor = null;
336
426
  }
337
427
  /**
338
- * Handle a request - main entrypoint for ServiceWorker usage
339
- * Returns a response or throws if no route matches
428
+ * Match a URL against registered routes
429
+ * Returns route info (params, methods, name, pattern) or null if no match
430
+ * Does not execute handlers - use handle() for that
340
431
  */
341
- handler;
342
- #handlerImpl;
432
+ match(url) {
433
+ const executor = this.#ensureCompiled();
434
+ return executor.matchURL(url);
435
+ }
343
436
  /**
344
- * Match a request against registered routes and execute the handler chain
345
- * Returns the response from the matched handler, or null if no route matches
346
- * Note: Global middleware executes even if no route matches
437
+ * Handle a request - main entrypoint for ServiceWorker usage
438
+ * Executes the matched handler with middleware chain
347
439
  */
348
- async match(request) {
349
- if (this.#dirty || !this.#executor) {
350
- this.#executor = new RadixTreeExecutor(this.#routes);
351
- this.#dirty = false;
352
- }
353
- let matchResult = this.#executor.match(request);
354
- let handler;
355
- let context;
356
- if (matchResult) {
357
- handler = matchResult.handler;
358
- context = matchResult.context;
359
- } else {
360
- handler = async () => {
361
- throw new NotFound();
362
- };
363
- context = { params: {} };
364
- }
365
- let response;
440
+ async handle(request) {
441
+ const executor = this.#ensureCompiled();
366
442
  try {
367
- response = await this.#executeMiddlewareStack(
368
- this.#middlewares,
443
+ const matchResult = executor.matchRequest(request);
444
+ let handler;
445
+ let context;
446
+ let routeMiddleware = [];
447
+ if (matchResult) {
448
+ if (!matchResult.handler) {
449
+ throw new NotFound("Route has no handler");
450
+ }
451
+ handler = matchResult.handler;
452
+ context = matchResult.context;
453
+ routeMiddleware = matchResult.entry.middlewares;
454
+ } else {
455
+ handler = async () => {
456
+ throw new NotFound();
457
+ };
458
+ context = { params: {} };
459
+ }
460
+ let response = await this.#executeMiddlewareStack(
461
+ this.middlewares,
462
+ routeMiddleware,
369
463
  request,
370
464
  context,
371
465
  handler
372
466
  );
373
- } catch (error) {
374
- if (!matchResult && isHTTPError(error) && error.status === 404) {
375
- return null;
467
+ if (request.method.toUpperCase() === "HEAD") {
468
+ response = new Response(null, {
469
+ status: response.status,
470
+ statusText: response.statusText,
471
+ headers: response.headers
472
+ });
376
473
  }
377
- throw error;
378
- }
379
- if (!matchResult && response?.status === 404) {
380
- return null;
381
- }
382
- if (response && request.method.toUpperCase() === "HEAD") {
383
- response = new Response(null, {
384
- status: response.status,
385
- statusText: response.statusText,
386
- headers: response.headers
387
- });
474
+ return response;
475
+ } catch (error) {
476
+ return this.#createErrorResponse(error);
388
477
  }
389
- return response;
390
- }
391
- /**
392
- * Get registered routes for debugging/introspection
393
- */
394
- getRoutes() {
395
- return [...this.#routes];
396
- }
397
- /**
398
- * Get registered middleware for debugging/introspection
399
- */
400
- getMiddlewares() {
401
- return [...this.#middlewares];
402
478
  }
403
479
  /**
404
480
  * Mount a subrouter at a specific path prefix
@@ -415,19 +491,21 @@ var Router = class {
415
491
  */
416
492
  mount(mountPath, subrouter) {
417
493
  const normalizedMountPath = this.#normalizeMountPath(mountPath);
418
- const subroutes = subrouter.getRoutes();
494
+ const subroutes = subrouter.routes;
419
495
  for (const subroute of subroutes) {
420
496
  const mountedPattern = this.#combinePaths(
421
497
  normalizedMountPath,
422
498
  subroute.pattern.pathname
423
499
  );
424
- this.#routes.push({
500
+ this.routes.push({
425
501
  pattern: new MatchPattern(mountedPattern),
426
502
  method: subroute.method,
427
- handler: subroute.handler
503
+ handler: subroute.handler,
504
+ name: subroute.name,
505
+ middlewares: subroute.middlewares
428
506
  });
429
507
  }
430
- const submiddlewares = subrouter.getMiddlewares();
508
+ const submiddlewares = subrouter.middlewares;
431
509
  for (const submiddleware of submiddlewares) {
432
510
  let composedPrefix;
433
511
  if (submiddleware.pathPrefix) {
@@ -438,12 +516,12 @@ var Router = class {
438
516
  } else {
439
517
  composedPrefix = normalizedMountPath;
440
518
  }
441
- this.#middlewares.push({
519
+ this.middlewares.push({
442
520
  middleware: submiddleware.middleware,
443
521
  pathPrefix: composedPrefix
444
522
  });
445
523
  }
446
- this.#dirty = true;
524
+ this.#executor = null;
447
525
  }
448
526
  /**
449
527
  * Normalize mount path: ensure it starts with / and doesn't end with /
@@ -498,39 +576,58 @@ var Router = class {
498
576
  }
499
577
  return false;
500
578
  }
579
+ /**
580
+ * Execute a single middleware and track generator state
581
+ * Returns true if middleware short-circuited (returned Response early)
582
+ */
583
+ async #executeMiddleware(middleware, request, context, runningGenerators) {
584
+ if (this.#isGeneratorMiddleware(middleware)) {
585
+ const generator = middleware(request, context);
586
+ const result = await generator.next();
587
+ if (result.done) {
588
+ if (result.value) {
589
+ return result.value;
590
+ }
591
+ } else {
592
+ runningGenerators.push({ generator });
593
+ }
594
+ } else {
595
+ const result = await middleware(request, context);
596
+ if (result) {
597
+ return result;
598
+ }
599
+ }
600
+ return null;
601
+ }
501
602
  /**
502
603
  * Execute middleware stack with guaranteed execution using Rack-style LIFO order
604
+ * Global/path middleware runs first, then route-scoped middleware, then handler
503
605
  */
504
- async #executeMiddlewareStack(middlewares, request, context, handler) {
606
+ async #executeMiddlewareStack(globalMiddlewares, routeMiddlewares, request, context, handler) {
505
607
  const runningGenerators = [];
506
608
  let currentResponse = null;
507
609
  const requestPathname = new URL(request.url).pathname;
508
- for (let i = 0; i < middlewares.length; i++) {
509
- const entry = middlewares[i];
510
- const middleware = entry.middleware;
610
+ for (const entry of globalMiddlewares) {
511
611
  if (entry.pathPrefix && !this.#matchesPathPrefix(requestPathname, entry.pathPrefix)) {
512
612
  continue;
513
613
  }
514
- if (this.#isGeneratorMiddleware(middleware)) {
515
- const generator = middleware(request, context);
516
- const result = await generator.next();
517
- if (result.done) {
518
- if (result.value) {
519
- currentResponse = result.value;
520
- break;
521
- }
522
- } else {
523
- runningGenerators.push({ generator, index: i });
524
- }
525
- } else {
526
- const result = await middleware(
614
+ currentResponse = await this.#executeMiddleware(
615
+ entry.middleware,
616
+ request,
617
+ context,
618
+ runningGenerators
619
+ );
620
+ if (currentResponse) break;
621
+ }
622
+ if (!currentResponse) {
623
+ for (const middleware of routeMiddlewares) {
624
+ currentResponse = await this.#executeMiddleware(
625
+ middleware,
527
626
  request,
528
- context
627
+ context,
628
+ runningGenerators
529
629
  );
530
- if (result) {
531
- currentResponse = result;
532
- break;
533
- }
630
+ if (currentResponse) break;
534
631
  }
535
632
  }
536
633
  if (!currentResponse) {
@@ -575,48 +672,20 @@ var Router = class {
575
672
  }
576
673
  throw error;
577
674
  }
578
- /**
579
- * Get route statistics
580
- */
581
- getStats() {
582
- return {
583
- routeCount: this.#routes.length,
584
- middlewareCount: this.#middlewares.length,
585
- compiled: !this.#dirty && this.#executor !== null
586
- };
587
- }
588
675
  /**
589
676
  * Create an error response for unhandled errors
590
677
  * Uses HTTPError.toResponse() for consistent error formatting
591
678
  */
592
679
  #createErrorResponse(error) {
593
- const httpError = isHTTPError(error) ? error : new InternalServerError(error.message, { cause: error });
594
680
  const isDev = import.meta.env?.MODE !== "production";
681
+ if (isDev && !isHTTPError(error)) {
682
+ logger.error`Unhandled error: ${error}`;
683
+ }
684
+ const httpError = isHTTPError(error) ? error : new InternalServerError(error.message, { cause: error });
595
685
  return httpError.toResponse(isDev);
596
686
  }
597
687
  };
598
- function trailingSlash(mode) {
599
- return (request, _context) => {
600
- const url = new URL(request.url);
601
- const pathname = url.pathname;
602
- if (pathname === "/")
603
- return;
604
- let newPathname = null;
605
- if (mode === "strip" && pathname.endsWith("/")) {
606
- newPathname = pathname.slice(0, -1);
607
- } else if (mode === "add" && !pathname.endsWith("/")) {
608
- newPathname = pathname + "/";
609
- }
610
- if (newPathname) {
611
- url.pathname = newPathname;
612
- return new Response(null, {
613
- status: 301,
614
- headers: { Location: url.toString() }
615
- });
616
- }
617
- };
618
- }
619
688
  export {
620
- Router,
621
- trailingSlash
689
+ RouteBuilder,
690
+ Router
622
691
  };