@sveltejs/kit 1.0.0-next.298 → 1.0.0-next.299

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.
@@ -2,7 +2,7 @@ import { onMount, tick } from 'svelte';
2
2
  import { writable } from 'svelte/store';
3
3
  import { assets, set_paths } from '../paths.js';
4
4
  import Root from '__GENERATED__/root.svelte';
5
- import { routes, fallback } from '__GENERATED__/manifest.js';
5
+ import { components, dictionary, validators } from '__GENERATED__/client-manifest.js';
6
6
  import { init } from './singletons.js';
7
7
 
8
8
  /**
@@ -250,9 +250,108 @@ function initial_fetch(resource, opts) {
250
250
  return fetch(resource, opts);
251
251
  }
252
252
 
253
+ /** @param {string} key */
254
+ function parse_route_id(key) {
255
+ /** @type {string[]} */
256
+ const names = [];
257
+
258
+ /** @type {string[]} */
259
+ const types = [];
260
+
261
+ const pattern =
262
+ key === ''
263
+ ? /^\/$/
264
+ : new RegExp(
265
+ `^${decodeURIComponent(key)
266
+ .split('/')
267
+ .map((segment) => {
268
+ // special case — /[...rest]/ could contain zero segments
269
+ const match = /^\[\.\.\.(\w+)(?:=\w+)?\]$/.exec(segment);
270
+ if (match) {
271
+ names.push(match[1]);
272
+ types.push(match[2]);
273
+ return '(?:/(.*))?';
274
+ }
275
+
276
+ return (
277
+ '/' +
278
+ segment.replace(/\[(\.\.\.)?(\w+)(?:=(\w+))?\]/g, (m, rest, name, type) => {
279
+ names.push(name);
280
+ types.push(type);
281
+ return rest ? '(.*?)' : '([^/]+?)';
282
+ })
283
+ );
284
+ })
285
+ .join('')}/?$`
286
+ );
287
+
288
+ return { pattern, names, types };
289
+ }
290
+
291
+ /**
292
+ * @param {RegExpMatchArray} match
293
+ * @param {string[]} names
294
+ * @param {string[]} types
295
+ * @param {Record<string, import('types').ParamValidator>} validators
296
+ */
297
+ function exec(match, names, types, validators) {
298
+ /** @type {Record<string, string>} */
299
+ const params = {};
300
+
301
+ for (let i = 0; i < names.length; i += 1) {
302
+ const name = names[i];
303
+ const type = types[i];
304
+ const value = match[i + 1] || '';
305
+
306
+ if (type) {
307
+ const validator = validators[type];
308
+ if (!validator) throw new Error(`Missing "${type}" param validator`); // TODO do this ahead of time?
309
+
310
+ if (!validator(value)) return;
311
+ }
312
+
313
+ params[name] = value;
314
+ }
315
+
316
+ return params;
317
+ }
318
+
319
+ /**
320
+ * @param {import('types').CSRComponentLoader[]} components
321
+ * @param {Record<string, [number[], number[], 1?]>} dictionary
322
+ * @param {Record<string, (param: string) => boolean>} validators
323
+ * @returns {import('types').CSRRoute[]}
324
+ */
325
+ function parse(components, dictionary, validators) {
326
+ const routes = Object.entries(dictionary).map(([id, [a, b, has_shadow]]) => {
327
+ const { pattern, names, types } = parse_route_id(id);
328
+
329
+ return {
330
+ id,
331
+ /** @param {string} path */
332
+ exec: (path) => {
333
+ const match = pattern.exec(path);
334
+ if (match) return exec(match, names, types, validators);
335
+ },
336
+ a: a.map((n) => components[n]),
337
+ b: b.map((n) => components[n]),
338
+ has_shadow: !!has_shadow
339
+ };
340
+ });
341
+
342
+ return routes;
343
+ }
344
+
253
345
  const SCROLL_KEY = 'sveltekit:scroll';
254
346
  const INDEX_KEY = 'sveltekit:index';
255
347
 
348
+ const routes = parse(components, dictionary, validators);
349
+
350
+ // we import the root layout/error components eagerly, so that
351
+ // connectivity errors after initialisation don't nuke the app
352
+ const default_layout = components[0]();
353
+ const default_error = components[1]();
354
+
256
355
  // We track the scroll position associated with each history entry in sessionStorage,
257
356
  // rather than on history.state itself, because when navigation is driven by
258
357
  // popstate it's too late to update the scroll position associated with the
@@ -339,8 +438,7 @@ function create_client({ target, session, base, trailing_slash }) {
339
438
  if (!ready) return;
340
439
  session_id += 1;
341
440
 
342
- const intent = get_navigation_intent(new URL(location.href));
343
- update(intent, [], true);
441
+ update(new URL(location.href), [], true);
344
442
  });
345
443
  ready = true;
346
444
 
@@ -405,29 +503,31 @@ function create_client({ target, session, base, trailing_slash }) {
405
503
 
406
504
  /** @param {URL} url */
407
505
  async function prefetch(url) {
408
- if (!owns(url)) {
506
+ const intent = get_navigation_intent(url);
507
+
508
+ if (!intent) {
409
509
  throw new Error('Attempted to prefetch a URL that does not belong to this app');
410
510
  }
411
511
 
412
- const intent = get_navigation_intent(url);
413
-
414
- load_cache.promise = get_navigation_result(intent, false);
512
+ load_cache.promise = load_route(intent, false);
415
513
  load_cache.id = intent.id;
416
514
 
417
515
  return load_cache.promise;
418
516
  }
419
517
 
420
518
  /**
421
- * @param {import('./types').NavigationIntent} intent
519
+ * @param {URL} url
422
520
  * @param {string[]} redirect_chain
423
521
  * @param {boolean} no_cache
424
522
  * @param {{hash?: string, scroll: { x: number, y: number } | null, keepfocus: boolean, details: { replaceState: boolean, state: any } | null}} [opts]
425
523
  */
426
- async function update(intent, redirect_chain, no_cache, opts) {
524
+ async function update(url, redirect_chain, no_cache, opts) {
525
+ const intent = get_navigation_intent(url);
526
+
427
527
  const current_token = (token = {});
428
- let navigation_result = await get_navigation_result(intent, no_cache);
528
+ let navigation_result = intent && (await load_route(intent, no_cache));
429
529
 
430
- if (!navigation_result && intent.url.pathname === location.pathname) {
530
+ if (!navigation_result && url.pathname === location.pathname) {
431
531
  // this could happen in SPA fallback mode if the user navigated to
432
532
  // `/non-existent-page`. if we fall back to reloading the page, it
433
533
  // will create an infinite loop. so whereas we normally handle
@@ -435,13 +535,14 @@ function create_client({ target, session, base, trailing_slash }) {
435
535
  // we render a client-side error page instead
436
536
  navigation_result = await load_root_error_page({
437
537
  status: 404,
438
- error: new Error(`Not found: ${intent.url.pathname}`),
439
- url: intent.url
538
+ error: new Error(`Not found: ${url.pathname}`),
539
+ url,
540
+ routeId: null
440
541
  });
441
542
  }
442
543
 
443
544
  if (!navigation_result) {
444
- await native_navigation(intent.url);
545
+ await native_navigation(url);
445
546
  return; // unnecessary, but TypeScript prefers it this way
446
547
  }
447
548
 
@@ -451,17 +552,18 @@ function create_client({ target, session, base, trailing_slash }) {
451
552
  invalidated.clear();
452
553
 
453
554
  if (navigation_result.redirect) {
454
- if (redirect_chain.length > 10 || redirect_chain.includes(intent.url.pathname)) {
555
+ if (redirect_chain.length > 10 || redirect_chain.includes(url.pathname)) {
455
556
  navigation_result = await load_root_error_page({
456
557
  status: 500,
457
558
  error: new Error('Redirect loop'),
458
- url: intent.url
559
+ url,
560
+ routeId: null
459
561
  });
460
562
  } else {
461
563
  if (router_enabled) {
462
- goto(new URL(navigation_result.redirect, intent.url).href, {}, [
564
+ goto(new URL(navigation_result.redirect, url).href, {}, [
463
565
  ...redirect_chain,
464
- intent.url.pathname
566
+ url.pathname
465
567
  ]);
466
568
  } else {
467
569
  await native_navigation(new URL(navigation_result.redirect, location.href));
@@ -472,7 +574,7 @@ function create_client({ target, session, base, trailing_slash }) {
472
574
  } else if (navigation_result.props?.page?.status >= 400) {
473
575
  const updated = await stores.updated.check();
474
576
  if (updated) {
475
- await native_navigation(intent.url);
577
+ await native_navigation(url);
476
578
  }
477
579
  }
478
580
 
@@ -482,7 +584,7 @@ function create_client({ target, session, base, trailing_slash }) {
482
584
  const { details } = opts;
483
585
  const change = details.replaceState ? 0 : 1;
484
586
  details.state[INDEX_KEY] = current_history_index += change;
485
- history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', intent.url);
587
+ history[details.replaceState ? 'replaceState' : 'pushState'](details.state, '', url);
486
588
  }
487
589
 
488
590
  if (started) {
@@ -522,7 +624,7 @@ function create_client({ target, session, base, trailing_slash }) {
522
624
  await tick();
523
625
 
524
626
  if (autoscroll) {
525
- const deep_linked = intent.url.hash && document.getElementById(intent.url.hash.slice(1));
627
+ const deep_linked = url.hash && document.getElementById(url.hash.slice(1));
526
628
  if (scroll) {
527
629
  scrollTo(scroll.x, scroll.y);
528
630
  } else if (deep_linked) {
@@ -575,36 +677,6 @@ function create_client({ target, session, base, trailing_slash }) {
575
677
  }
576
678
  }
577
679
 
578
- /**
579
- * @param {import('./types').NavigationIntent} intent
580
- * @param {boolean} no_cache
581
- */
582
- async function get_navigation_result(intent, no_cache) {
583
- if (load_cache.id === intent.id && load_cache.promise) {
584
- return load_cache.promise;
585
- }
586
-
587
- for (let i = 0; i < intent.routes.length; i += 1) {
588
- const route = intent.routes[i];
589
-
590
- // load code for subsequent routes immediately, if they are as
591
- // likely to match the current path/query as the current one
592
- let j = i + 1;
593
- while (j < intent.routes.length) {
594
- const next = intent.routes[j];
595
- if (next[0].toString() === route[0].toString()) {
596
- next[1].forEach((loader) => loader());
597
- j += 1;
598
- } else {
599
- break;
600
- }
601
- }
602
-
603
- const result = await load_route(route, intent, no_cache);
604
- if (result) return result;
605
- }
606
- }
607
-
608
680
  /**
609
681
  *
610
682
  * @param {{
@@ -614,9 +686,18 @@ function create_client({ target, session, base, trailing_slash }) {
614
686
  * branch: Array<import('./types').BranchNode | undefined>;
615
687
  * status: number;
616
688
  * error?: Error;
689
+ * routeId: string | null;
617
690
  * }} opts
618
691
  */
619
- async function get_navigation_result_from_branch({ url, params, stuff, branch, status, error }) {
692
+ async function get_navigation_result_from_branch({
693
+ url,
694
+ params,
695
+ stuff,
696
+ branch,
697
+ status,
698
+ error,
699
+ routeId
700
+ }) {
620
701
  const filtered = /** @type {import('./types').BranchNode[] } */ (branch.filter(Boolean));
621
702
  const redirect = filtered.find((f) => f.loaded?.redirect);
622
703
 
@@ -640,7 +721,7 @@ function create_client({ target, session, base, trailing_slash }) {
640
721
  }
641
722
 
642
723
  if (!current.url || url.href !== current.url.href) {
643
- result.props.page = { url, params, status, error, stuff };
724
+ result.props.page = { error, params, routeId, status, stuff, url };
644
725
 
645
726
  // TODO remove this for 1.0
646
727
  /**
@@ -699,9 +780,10 @@ function create_client({ target, session, base, trailing_slash }) {
699
780
  * params: Record<string, string>;
700
781
  * stuff: Record<string, any>;
701
782
  * props?: Record<string, any>;
783
+ * routeId: string | null;
702
784
  * }} options
703
785
  */
704
- async function load_node({ status, error, module, url, params, stuff, props }) {
786
+ async function load_node({ status, error, module, url, params, stuff, props, routeId }) {
705
787
  /** @type {import('./types').BranchNode} */
706
788
  const node = {
707
789
  module,
@@ -738,6 +820,7 @@ function create_client({ target, session, base, trailing_slash }) {
738
820
  if (module.load) {
739
821
  /** @type {import('types').LoadInput | import('types').ErrorLoadInput} */
740
822
  const load_input = {
823
+ routeId,
741
824
  params: uses_params,
742
825
  props: props || {},
743
826
  get url() {
@@ -791,21 +874,20 @@ function create_client({ target, session, base, trailing_slash }) {
791
874
  }
792
875
 
793
876
  /**
794
- * @param {import('types').CSRRoute} route
795
877
  * @param {import('./types').NavigationIntent} intent
796
878
  * @param {boolean} no_cache
797
879
  */
798
- async function load_route(route, { id, url, path, routes }, no_cache) {
880
+ async function load_route({ id, url, params, route }, no_cache) {
881
+ if (load_cache.id === id && load_cache.promise) {
882
+ return load_cache.promise;
883
+ }
884
+
799
885
  if (!no_cache) {
800
886
  const cached = cache.get(id);
801
887
  if (cached) return cached;
802
888
  }
803
889
 
804
- const [pattern, a, b, get_params, shadow_key] = route;
805
- const params = get_params
806
- ? // the pattern is for the route which we've already matched to this path
807
- get_params(/** @type {RegExpExecArray} */ (pattern.exec(path)))
808
- : {};
890
+ const { a, b, has_shadow } = route;
809
891
 
810
892
  const changed = current.url && {
811
893
  url: id !== current.url.pathname + current.url.search,
@@ -852,14 +934,14 @@ function create_client({ target, session, base, trailing_slash }) {
852
934
  /** @type {Record<string, any>} */
853
935
  let props = {};
854
936
 
855
- const is_shadow_page = shadow_key !== undefined && i === a.length - 1;
937
+ const is_shadow_page = has_shadow && i === a.length - 1;
856
938
 
857
939
  if (is_shadow_page) {
858
940
  const res = await fetch(
859
941
  `${url.pathname}${url.pathname.endsWith('/') ? '' : '/'}__data.json${url.search}`,
860
942
  {
861
943
  headers: {
862
- 'x-sveltekit-load': /** @type {string} */ (shadow_key)
944
+ 'x-sveltekit-load': 'true'
863
945
  }
864
946
  }
865
947
  );
@@ -875,15 +957,7 @@ function create_client({ target, session, base, trailing_slash }) {
875
957
  };
876
958
  }
877
959
 
878
- if (res.status === 204) {
879
- if (route !== routes[routes.length - 1]) {
880
- // fallthrough
881
- return;
882
- }
883
- props = {};
884
- } else {
885
- props = await res.json();
886
- }
960
+ props = res.status === 204 ? {} : await res.json();
887
961
  } else {
888
962
  status = res.status;
889
963
  error = new Error('Failed to load data');
@@ -896,7 +970,8 @@ function create_client({ target, session, base, trailing_slash }) {
896
970
  url,
897
971
  params,
898
972
  props,
899
- stuff
973
+ stuff,
974
+ routeId: route.id
900
975
  });
901
976
  }
902
977
 
@@ -906,9 +981,14 @@ function create_client({ target, session, base, trailing_slash }) {
906
981
  }
907
982
 
908
983
  if (node.loaded) {
984
+ // TODO remove for 1.0
985
+ // @ts-expect-error
909
986
  if (node.loaded.fallthrough) {
910
- return;
987
+ throw new Error(
988
+ 'fallthrough is no longer supported. Use validators instead: https://kit.svelte.dev/docs/routing#advanced-routing-validation'
989
+ );
911
990
  }
991
+
912
992
  if (node.loaded.error) {
913
993
  status = node.loaded.status;
914
994
  error = node.loaded.error;
@@ -954,7 +1034,8 @@ function create_client({ target, session, base, trailing_slash }) {
954
1034
  module: await b[i](),
955
1035
  url,
956
1036
  params,
957
- stuff: node_loaded.stuff
1037
+ stuff: node_loaded.stuff,
1038
+ routeId: route.id
958
1039
  });
959
1040
 
960
1041
  if (error_loaded?.loaded?.error) {
@@ -979,7 +1060,8 @@ function create_client({ target, session, base, trailing_slash }) {
979
1060
  return await load_root_error_page({
980
1061
  status,
981
1062
  error,
982
- url
1063
+ url,
1064
+ routeId: route.id
983
1065
  });
984
1066
  } else {
985
1067
  if (node?.loaded?.stuff) {
@@ -999,7 +1081,8 @@ function create_client({ target, session, base, trailing_slash }) {
999
1081
  stuff,
1000
1082
  branch,
1001
1083
  status,
1002
- error
1084
+ error,
1085
+ routeId: route.id
1003
1086
  });
1004
1087
  }
1005
1088
 
@@ -1008,26 +1091,29 @@ function create_client({ target, session, base, trailing_slash }) {
1008
1091
  * status: number;
1009
1092
  * error: Error;
1010
1093
  * url: URL;
1094
+ * routeId: string | null
1011
1095
  * }} opts
1012
1096
  */
1013
- async function load_root_error_page({ status, error, url }) {
1097
+ async function load_root_error_page({ status, error, url, routeId }) {
1014
1098
  /** @type {Record<string, string>} */
1015
1099
  const params = {}; // error page does not have params
1016
1100
 
1017
1101
  const root_layout = await load_node({
1018
- module: await fallback[0],
1102
+ module: await default_layout,
1019
1103
  url,
1020
1104
  params,
1021
- stuff: {}
1105
+ stuff: {},
1106
+ routeId
1022
1107
  });
1023
1108
 
1024
1109
  const root_error = await load_node({
1025
1110
  status,
1026
1111
  error,
1027
- module: await fallback[1],
1112
+ module: await default_error,
1028
1113
  url,
1029
1114
  params,
1030
- stuff: (root_layout && root_layout.loaded && root_layout.loaded.stuff) || {}
1115
+ stuff: (root_layout && root_layout.loaded && root_layout.loaded.stuff) || {},
1116
+ routeId
1031
1117
  });
1032
1118
 
1033
1119
  return await get_navigation_result_from_branch({
@@ -1039,28 +1125,32 @@ function create_client({ target, session, base, trailing_slash }) {
1039
1125
  },
1040
1126
  branch: [root_layout, root_error],
1041
1127
  status,
1042
- error
1128
+ error,
1129
+ routeId
1043
1130
  });
1044
1131
  }
1045
1132
 
1046
- /** @param {URL} url */
1047
- function owns(url) {
1048
- return url.origin === location.origin && url.pathname.startsWith(base);
1049
- }
1050
-
1051
1133
  /** @param {URL} url */
1052
1134
  function get_navigation_intent(url) {
1135
+ if (url.origin !== location.origin || !url.pathname.startsWith(base)) return;
1136
+
1053
1137
  const path = decodeURI(url.pathname.slice(base.length) || '/');
1054
1138
 
1055
- /** @type {import('./types').NavigationIntent} */
1056
- const intent = {
1057
- id: url.pathname + url.search,
1058
- routes: routes.filter(([pattern]) => pattern.test(path)),
1059
- url,
1060
- path
1061
- };
1139
+ for (const route of routes) {
1140
+ const params = route.exec(path);
1141
+
1142
+ if (params) {
1143
+ /** @type {import('./types').NavigationIntent} */
1144
+ const intent = {
1145
+ id: url.pathname + url.search,
1146
+ route,
1147
+ params,
1148
+ url
1149
+ };
1062
1150
 
1063
- return intent;
1151
+ return intent;
1152
+ }
1153
+ }
1064
1154
  }
1065
1155
 
1066
1156
  /**
@@ -1094,14 +1184,8 @@ function create_client({ target, session, base, trailing_slash }) {
1094
1184
  return;
1095
1185
  }
1096
1186
 
1097
- if (!owns(url)) {
1098
- await native_navigation(url);
1099
- }
1100
-
1101
1187
  const pathname = normalize_path(url.pathname, trailing_slash);
1102
- url = new URL(url.origin + pathname + url.search + url.hash);
1103
-
1104
- const intent = get_navigation_intent(url);
1188
+ const normalized = new URL(url.origin + pathname + url.search + url.hash);
1105
1189
 
1106
1190
  update_scroll_positions(current_history_index);
1107
1191
 
@@ -1114,11 +1198,11 @@ function create_client({ target, session, base, trailing_slash }) {
1114
1198
  if (started) {
1115
1199
  stores.navigating.set({
1116
1200
  from: current.url,
1117
- to: intent.url
1201
+ to: normalized
1118
1202
  });
1119
1203
  }
1120
1204
 
1121
- await update(intent, redirect_chain, false, {
1205
+ await update(normalized, redirect_chain, false, {
1122
1206
  scroll,
1123
1207
  keepfocus,
1124
1208
  details
@@ -1130,7 +1214,7 @@ function create_client({ target, session, base, trailing_slash }) {
1130
1214
  if (navigating_token !== current_navigating_token) return;
1131
1215
 
1132
1216
  if (!navigating) {
1133
- const navigation = { from, to: url };
1217
+ const navigation = { from, to: normalized };
1134
1218
  callbacks.after_navigate.forEach((fn) => fn(navigation));
1135
1219
 
1136
1220
  stores.navigating.set(null);
@@ -1190,8 +1274,7 @@ function create_client({ target, session, base, trailing_slash }) {
1190
1274
 
1191
1275
  if (!invalidating) {
1192
1276
  invalidating = Promise.resolve().then(async () => {
1193
- const intent = get_navigation_intent(new URL(location.href));
1194
- await update(intent, [], true);
1277
+ await update(new URL(location.href), [], true);
1195
1278
 
1196
1279
  invalidating = null;
1197
1280
  });
@@ -1208,10 +1291,10 @@ function create_client({ target, session, base, trailing_slash }) {
1208
1291
  // TODO rethink this API
1209
1292
  prefetch_routes: async (pathnames) => {
1210
1293
  const matching = pathnames
1211
- ? routes.filter((route) => pathnames.some((pathname) => route[0].test(pathname)))
1294
+ ? routes.filter((route) => pathnames.some((pathname) => route.exec(pathname)))
1212
1295
  : routes;
1213
1296
 
1214
- const promises = matching.map((r) => Promise.all(r[1].map((load) => load())));
1297
+ const promises = matching.map((r) => Promise.all(r.a.map((load) => load())));
1215
1298
 
1216
1299
  await Promise.all(promises);
1217
1300
  },
@@ -1389,7 +1472,7 @@ function create_client({ target, session, base, trailing_slash }) {
1389
1472
  });
1390
1473
  },
1391
1474
 
1392
- _hydrate: async ({ status, error, nodes, params }) => {
1475
+ _hydrate: async ({ status, error, nodes, params, routeId }) => {
1393
1476
  const url = new URL(location.href);
1394
1477
 
1395
1478
  /** @type {Array<import('./types').BranchNode | undefined>} */
@@ -1423,7 +1506,8 @@ function create_client({ target, session, base, trailing_slash }) {
1423
1506
  stuff,
1424
1507
  status: is_leaf ? status : undefined,
1425
1508
  error: is_leaf ? error : undefined,
1426
- props
1509
+ props,
1510
+ routeId
1427
1511
  });
1428
1512
 
1429
1513
  if (props) {
@@ -1439,7 +1523,8 @@ function create_client({ target, session, base, trailing_slash }) {
1439
1523
  error_args = {
1440
1524
  status: node.loaded.status,
1441
1525
  error: node.loaded.error,
1442
- url
1526
+ url,
1527
+ routeId
1443
1528
  };
1444
1529
  } else if (node.loaded.stuff) {
1445
1530
  stuff = {
@@ -1458,7 +1543,8 @@ function create_client({ target, session, base, trailing_slash }) {
1458
1543
  stuff,
1459
1544
  branch,
1460
1545
  status,
1461
- error
1546
+ error,
1547
+ routeId
1462
1548
  });
1463
1549
  } catch (e) {
1464
1550
  if (error) throw e;
@@ -1466,7 +1552,8 @@ function create_client({ target, session, base, trailing_slash }) {
1466
1552
  result = await load_root_error_page({
1467
1553
  status: 500,
1468
1554
  error: coalesce_to_error(e),
1469
- url
1555
+ url,
1556
+ routeId
1470
1557
  });
1471
1558
  }
1472
1559
 
@@ -1497,6 +1584,7 @@ function create_client({ target, session, base, trailing_slash }) {
1497
1584
  * error: Error;
1498
1585
  * nodes: Array<Promise<import('types').CSRComponent>>;
1499
1586
  * params: Record<string, string>;
1587
+ * routeId: string | null;
1500
1588
  * };
1501
1589
  * }} opts
1502
1590
  */