@hybridly/core 0.10.0-beta.12 → 0.10.0-beta.13

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.
@@ -1,4 +1,4 @@
1
- import "node:module";
1
+ //#region \0rolldown/runtime.js
2
2
  var __defProp = Object.defineProperty;
3
3
  var __exportAll = (all, no_symbols) => {
4
4
  let target = {};
@@ -9,4 +9,5 @@ var __exportAll = (all, no_symbols) => {
9
9
  if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
10
10
  return target;
11
11
  };
12
+ //#endregion
12
13
  export { __exportAll as t };
package/dist/index.mjs CHANGED
@@ -3,6 +3,7 @@ import { debounce, debug, hasFiles, match, merge, objectToFormData, random, remo
3
3
  import axios from "axios";
4
4
  import { parse, stringify } from "superjson";
5
5
  import qs from "qs";
6
+ //#region src/constants.ts
6
7
  var constants_exports = /* @__PURE__ */ __exportAll({
7
8
  DIALOG_KEY_HEADER: () => DIALOG_KEY_HEADER,
8
9
  DIALOG_REDIRECT_HEADER: () => DIALOG_REDIRECT_HEADER,
@@ -29,6 +30,8 @@ const EXCEPT_DATA_HEADER = `${HYBRIDLY_HEADER}-except-data`;
29
30
  const VERSION_HEADER = `${HYBRIDLY_HEADER}-version`;
30
31
  const ERROR_BAG_HEADER = `${HYBRIDLY_HEADER}-error-bag`;
31
32
  const SCROLL_REGION_ATTRIBUTE = "scroll-region";
33
+ //#endregion
34
+ //#region src/errors.ts
32
35
  var NotAHybridResponseError = class extends Error {
33
36
  constructor(response) {
34
37
  super();
@@ -51,6 +54,8 @@ var MissingRouteParameter = class extends Error {
51
54
  super(`Parameter [${parameter}] is required for route [${routeName}].`);
52
55
  }
53
56
  };
57
+ //#endregion
58
+ //#region src/plugins/plugin.ts
54
59
  function definePlugin(plugin) {
55
60
  return plugin;
56
61
  }
@@ -87,6 +92,11 @@ async function runHooks(hook, requestHooks, ...args) {
87
92
  debug.hook(`Called all hooks for [${hook}],`, result);
88
93
  return !result.includes(false);
89
94
  }
95
+ //#endregion
96
+ //#region src/plugins/hooks.ts
97
+ /**
98
+ * Registers a global hook.
99
+ */
90
100
  function appendCallbackToHooks(hook, fn) {
91
101
  const hooks = getRouterContext().hooks;
92
102
  hooks[hook] = [...hooks[hook] ?? [], fn];
@@ -95,6 +105,9 @@ function appendCallbackToHooks(hook, fn) {
95
105
  if (index !== -1) hooks[hook]?.splice(index, 1);
96
106
  };
97
107
  }
108
+ /**
109
+ * Registers a global hook.
110
+ */
98
111
  function registerHook(hook, fn, options) {
99
112
  if (options?.once) {
100
113
  const unregister = appendCallbackToHooks(hook, async (...args) => {
@@ -105,6 +118,9 @@ function registerHook(hook, fn, options) {
105
118
  }
106
119
  return appendCallbackToHooks(hook, fn);
107
120
  }
121
+ //#endregion
122
+ //#region src/scroll.ts
123
+ /** Saves the current view's scrollbar positions into the history state. */
108
124
  function saveScrollPositions() {
109
125
  const regions = getScrollRegions();
110
126
  debug.scroll("Saving scroll positions of:", regions.map((el) => ({
@@ -120,9 +136,14 @@ function saveScrollPositions() {
120
136
  })) });
121
137
  setHistoryState({ replace: true });
122
138
  }
139
+ /** Gets DOM elements which scroll positions should be saved. */
123
140
  function getScrollRegions() {
124
- return Array.from(document?.querySelectorAll(`[${SCROLL_REGION_ATTRIBUTE}]`) ?? []).concat(document.documentElement, document.body);
141
+ return Array.from(document?.querySelectorAll(`[scroll-region]`) ?? []).concat(document.documentElement, document.body);
125
142
  }
143
+ /**
144
+ * Resets the current view's scrollbars positions to the top, and save them
145
+ * in the history state.
146
+ */
126
147
  function resetScrollPositions() {
127
148
  debug.scroll("Resetting scroll positions.");
128
149
  getScrollRegions().forEach((element) => element.scrollTo({
@@ -135,6 +156,7 @@ function resetScrollPositions() {
135
156
  document.getElementById(window.location.hash.slice(1))?.scrollIntoView();
136
157
  }
137
158
  }
159
+ /** Restores the scroll positions stored in the context. */
138
160
  async function restoreScrollPositions() {
139
161
  const context = getRouterContext();
140
162
  const regions = getScrollRegions();
@@ -150,9 +172,15 @@ async function restoreScrollPositions() {
150
172
  }));
151
173
  });
152
174
  }
175
+ //#endregion
176
+ //#region src/url.ts
177
+ /** Normalizes the given input to an URL. */
153
178
  function normalizeUrl(href, trailingSlash) {
154
179
  return makeUrl(href, { trailingSlash }).toString();
155
180
  }
181
+ /**
182
+ * Converts an input to an URL, optionally changing its properties after initialization.
183
+ */
156
184
  function makeUrl(href, transformations = {}) {
157
185
  try {
158
186
  const base = document?.location?.href === "//" ? void 0 : document.location.href;
@@ -179,6 +207,9 @@ function makeUrl(href, transformations = {}) {
179
207
  throw new TypeError(`${href} is not resolvable to a valid URL.`);
180
208
  }
181
209
  }
210
+ /**
211
+ * Checks if the given URLs have the same origin and path.
212
+ */
182
213
  function sameUrls(...hrefs) {
183
214
  if (hrefs.length < 2) return true;
184
215
  try {
@@ -188,6 +219,9 @@ function sameUrls(...hrefs) {
188
219
  } catch {}
189
220
  return false;
190
221
  }
222
+ /**
223
+ * Checks if the given URLs have the same origin, path, and hash.
224
+ */
191
225
  function sameHashes(...hrefs) {
192
226
  if (hrefs.length < 2) return true;
193
227
  try {
@@ -197,12 +231,20 @@ function sameHashes(...hrefs) {
197
231
  } catch {}
198
232
  return false;
199
233
  }
234
+ /**
235
+ * If the back-end did not specify a hash, if the navigation specified one,
236
+ * and both URLs lead to the same endpoint, we update the target URL
237
+ * to use the hash of the initially-requested URL.
238
+ */
200
239
  function fillHash(currentUrl, targetUrl) {
201
240
  currentUrl = makeUrl(currentUrl);
202
241
  targetUrl = makeUrl(targetUrl);
203
242
  if (currentUrl.hash && !targetUrl.hash && sameUrls(targetUrl, currentUrl)) targetUrl.hash = currentUrl.hash;
204
243
  return targetUrl.toString();
205
244
  }
245
+ //#endregion
246
+ //#region src/router/history.ts
247
+ /** Puts the given context into the history state. */
206
248
  function setHistoryState(options = {}) {
207
249
  if (!window?.history) throw new Error("The history API is not available, so Hybridly cannot operate.");
208
250
  const context = getRouterContext();
@@ -219,13 +261,16 @@ function setHistoryState(options = {}) {
219
261
  throw error;
220
262
  }
221
263
  }
264
+ /** Gets the current history data if it exists. */
222
265
  function getHistoryState() {
223
266
  return getRouterContext().serializer.unserialize(window.history.state);
224
267
  }
268
+ /** Gets the current history state if it exists. */
225
269
  function getHistoryMemo(key) {
226
270
  const state = getHistoryState();
227
271
  return key ? state?.memo?.[key] : state?.memo;
228
272
  }
273
+ /** Register history-related event listeneners. */
229
274
  async function registerEventListeners() {
230
275
  const context = getRouterContext();
231
276
  debug.history("Registering [popstate] and [scroll] event listeners.");
@@ -259,13 +304,15 @@ async function registerEventListeners() {
259
304
  });
260
305
  });
261
306
  window?.addEventListener("scroll", (event) => debounce(100, () => {
262
- if ((event?.target)?.hasAttribute?.(SCROLL_REGION_ATTRIBUTE)) saveScrollPositions();
307
+ if ((event?.target)?.hasAttribute?.("scroll-region")) saveScrollPositions();
263
308
  }), true);
264
309
  }
310
+ /** Checks if the current navigation was made by going back or forward. */
265
311
  function isBackForwardNavigation() {
266
312
  if (!window.history.state) return false;
267
313
  return (window.performance?.getEntriesByType("navigation").at(0))?.type === "back_forward";
268
314
  }
315
+ /** Handles a navigation which was going back or forward. */
269
316
  async function handleBackForwardNavigation() {
270
317
  debug.router("Handling a back/forward navigation from an external URL.");
271
318
  const context = getRouterContext();
@@ -282,6 +329,7 @@ async function handleBackForwardNavigation() {
282
329
  updateHistoryState: false
283
330
  });
284
331
  }
332
+ /** Saves a value into the current history state. */
285
333
  function remember(key, value) {
286
334
  debug.history(`Remembering key "${key}" with value`, value);
287
335
  setContext({ memo: {
@@ -290,6 +338,7 @@ function remember(key, value) {
290
338
  } }, { propagate: false });
291
339
  setHistoryState({ replace: true });
292
340
  }
341
+ /** Serializes the context so it can be written to the history state. */
293
342
  function serializeContext(context) {
294
343
  return context.serializer.serialize({
295
344
  url: context.url,
@@ -316,6 +365,8 @@ function createSerializer(options) {
316
365
  }
317
366
  };
318
367
  }
368
+ //#endregion
369
+ //#region src/routing/route.ts
319
370
  function getUrlRegexForRoute(name) {
320
371
  const routing = getRouting();
321
372
  const definition = getRouteDefinition(name);
@@ -333,6 +384,12 @@ function getUrlRegexForRoute(name) {
333
384
  });
334
385
  return RegExp(urlRegexPattern);
335
386
  }
387
+ /**
388
+ * Check if a given URL matches a route based on its name.
389
+ * Additionally you can pass an object of parameters to check if the URL matches the route with the given parameters.
390
+ * Otherwise it will accept and thus return true for any values for the parameters defined by the route.
391
+ * Note: passing additional parameters that are not defined by the route or included in the current URL will cause this to return false.
392
+ */
336
393
  function urlMatchesRoute(fullUrl, name, routeParameters) {
337
394
  const url = makeUrl(fullUrl, { hash: "" }).toString();
338
395
  const parameters = routeParameters || {};
@@ -369,6 +426,9 @@ function getUrlFromName(name, parameters, shouldThrow) {
369
426
  ...transforms
370
427
  }));
371
428
  }
429
+ /**
430
+ * Resolved the value of a route parameter from either the passed parameters or the default parameters.
431
+ */
372
432
  function getRouteParameterValue(routeName, parameterName, routeParameters) {
373
433
  const routing = getRouting();
374
434
  const definition = getRouteDefinition(routeName);
@@ -386,6 +446,9 @@ function getRouteParameterValue(routeName, parameterName, routeParameters) {
386
446
  }
387
447
  if (routing.defaults?.[parameterName]) return routing.defaults?.[parameterName];
388
448
  }
449
+ /**
450
+ * Gets the `UrlTransformable` object for the given route and parameters.
451
+ */
389
452
  function getRouteTransformable(routeName, routeParameters, shouldThrow) {
390
453
  const definition = getRouteDefinition(routeName);
391
454
  const parameters = routeParameters || {};
@@ -415,23 +478,44 @@ function getRouteTransformable(routeName, routeParameters, shouldThrow) {
415
478
  })
416
479
  };
417
480
  }
481
+ /**
482
+ * Gets the route definition.
483
+ */
418
484
  function getRouteDefinition(name) {
419
485
  const definition = getRouting().routes[name];
420
486
  if (!definition) throw new RouteNotFound(name);
421
487
  return definition;
422
488
  }
489
+ /**
490
+ * Gets the routing configuration from the current context.
491
+ */
423
492
  function getRouting() {
424
493
  const { routing } = getInternalRouterContext();
425
494
  if (!routing) throw new RoutingNotInitialized();
426
495
  return routing;
427
496
  }
497
+ /**
498
+ * Generates a route from the given route name.
499
+ */
428
500
  function route(name, parameters, absolute) {
429
501
  return generateRouteFromName(name, parameters, absolute ?? getRouting().absolute ?? true);
430
502
  }
503
+ //#endregion
504
+ //#region src/routing/current.ts
431
505
  function getCurrentUrl() {
432
506
  if (typeof window === "undefined") return getInternalRouterContext().url;
433
507
  return window.location.toString();
434
508
  }
509
+ /**
510
+ * Determines whether the current route matches the given name and parameters.
511
+ * If multiple routes match, the first one will be returned.
512
+ *
513
+ * @example
514
+ * ```ts
515
+ * currentRouteMatches('tenant.*') // matches all routes starting with 'tenant.'
516
+ * currentRouteMatches('tenant.*.admin') // matches all routes starting with 'tenant.' and ending with '.admin'
517
+ * ```
518
+ */
435
519
  function currentRouteMatches(name, parameters) {
436
520
  const namePattern = `^${name.replaceAll(".", "\\.").replaceAll("*", ".*")}$`;
437
521
  const possibleRoutes = Object.values(getRouting().routes).filter((x) => x.method.includes("GET") && RegExp(namePattern).test(x.name)).map((x) => x.name);
@@ -443,13 +527,19 @@ function currentRouteMatches(name, parameters) {
443
527
  function getCurrentRouteName() {
444
528
  return getNameFromUrl(getCurrentUrl());
445
529
  }
530
+ //#endregion
531
+ //#region src/routing/index.ts
446
532
  function updateRoutingConfiguration(routing) {
447
533
  if (!routing) return;
448
534
  setContext({ routing });
449
535
  }
536
+ //#endregion
537
+ //#region src/download.ts
538
+ /** Checks if the response wants to redirect to an external URL. */
450
539
  function isDownloadResponse(response) {
451
540
  return response.status === 200 && !!response.headers["content-disposition"];
452
541
  }
542
+ /** Handles a download. */
453
543
  async function handleDownloadResponse(response) {
454
544
  const blob = new Blob([response.data], { type: response.headers["content-type"] });
455
545
  const urlObject = window.webkitURL || window.URL;
@@ -466,17 +556,22 @@ async function handleDownloadResponse(response) {
466
556
  function getFileNameFromContentDispositionHeader(header) {
467
557
  return (header.split(";")[1]?.trim().split("=")[1])?.replace(/^"(.*)"$/, "$1") ?? "";
468
558
  }
559
+ //#endregion
560
+ //#region src/context/context.ts
469
561
  const state = {
470
562
  initialized: false,
471
563
  context: {}
472
564
  };
565
+ /** Gets the current context. */
473
566
  function getRouterContext() {
474
567
  return getInternalRouterContext();
475
568
  }
569
+ /** Gets the current context, but not in read-only. */
476
570
  function getInternalRouterContext() {
477
571
  if (!state.initialized) throw new Error("Hybridly is not initialized.");
478
572
  return state.context;
479
573
  }
574
+ /** Initializes the context. */
480
575
  async function initializeContext(options) {
481
576
  state.initialized = true;
482
577
  state.context = {
@@ -499,6 +594,10 @@ async function initializeContext(options) {
499
594
  await runHooks("initialized", {}, state.context);
500
595
  return getInternalRouterContext();
501
596
  }
597
+ /**
598
+ * Registers an interceptor that assumes `arraybuffer`
599
+ * responses and converts responses to JSON or text.
600
+ */
502
601
  function registerAxios(axios) {
503
602
  axios.interceptors.response.use((response) => {
504
603
  if (!isDownloadResponse(response)) {
@@ -513,6 +612,9 @@ function registerAxios(axios) {
513
612
  }, (error) => Promise.reject(error));
514
613
  return axios;
515
614
  }
615
+ /**
616
+ * Mutates properties at the top-level of the context.
617
+ */
516
618
  function setContext(merge = {}, options = {}) {
517
619
  Object.keys(merge).forEach((key) => {
518
620
  Reflect.set(state.context, key, merge[key]);
@@ -523,6 +625,7 @@ function setContext(merge = {}, options = {}) {
523
625
  added: merge
524
626
  });
525
627
  }
628
+ /** Gets a payload from the current context. */
526
629
  function payloadFromContext() {
527
630
  return {
528
631
  url: getRouterContext().url,
@@ -531,6 +634,13 @@ function payloadFromContext() {
531
634
  dialog: getRouterContext().dialog
532
635
  };
533
636
  }
637
+ //#endregion
638
+ //#region src/external.ts
639
+ /**
640
+ * Performs an external navigation by saving options to the storage and
641
+ * making a full page reload. Upon loading, the navigation options
642
+ * will be pulled and a hybrid navigation will be made.
643
+ */
534
644
  async function performExternalNavigation(options) {
535
645
  debug.external("Navigating to an external URL:", options);
536
646
  if (options.target === "new-tab") {
@@ -549,18 +659,24 @@ async function performExternalNavigation(options) {
549
659
  window.location.reload();
550
660
  }
551
661
  }
662
+ /** Navigates to the given URL without the hybrid protocol. */
552
663
  function navigateToExternalUrl(url, data) {
553
664
  document.location.href = makeUrl(url, { search: qs.stringify(data, {
554
665
  encodeValuesOnly: true,
555
666
  arrayFormat: "brackets"
556
667
  }) }).toString();
557
668
  }
669
+ /** Checks if the response wants to redirect to an external URL. */
558
670
  function isExternalResponse(response) {
559
671
  return response?.status === 409 && !!response?.headers?.[EXTERNAL_NAVIGATION_HEADER];
560
672
  }
673
+ /**
674
+ * Performs the internal navigation when an external navigation to a hybrid view has been made.
675
+ * This method is meant to be called on router creation.
676
+ */
561
677
  async function handleExternalNavigation() {
562
678
  debug.external("Handling an external navigation.");
563
- const options = JSON.parse(window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) || "{}");
679
+ const options = JSON.parse(window.sessionStorage.getItem("hybridly:external") || "{}");
564
680
  window.sessionStorage.removeItem(STORAGE_EXTERNAL_KEY);
565
681
  debug.external("Options from the session storage:", options);
566
682
  setContext({ url: makeUrl(getRouterContext().url, { hash: window.location.hash }).toString() });
@@ -570,12 +686,18 @@ async function handleExternalNavigation() {
570
686
  preserveScroll: options.preserveScroll
571
687
  });
572
688
  }
689
+ /** Checks if the navigation being initialized points to an external location. */
573
690
  function isExternalNavigation() {
574
691
  try {
575
692
  return window.sessionStorage.getItem(STORAGE_EXTERNAL_KEY) !== null;
576
693
  } catch {}
577
694
  return false;
578
695
  }
696
+ //#endregion
697
+ //#region src/dialog/index.ts
698
+ /**
699
+ * Closes the dialog.
700
+ */
579
701
  async function closeDialog(options) {
580
702
  const context = getInternalRouterContext();
581
703
  const url = context.dialog?.redirectUrl ?? context.dialog?.baseUrl;
@@ -596,18 +718,33 @@ async function closeDialog(options) {
596
718
  ...options
597
719
  });
598
720
  }
721
+ //#endregion
722
+ //#region src/router/preload.ts
723
+ /**
724
+ * Checks if there is a preloaded request for the given URL.
725
+ */
599
726
  function isPreloaded(targetUrl) {
600
727
  return getInternalRouterContext().preloadCache.has(targetUrl.toString()) ?? false;
601
728
  }
729
+ /**
730
+ * Gets the response of a preloaded request.
731
+ */
602
732
  function getPreloadedRequest(targetUrl) {
603
733
  return getInternalRouterContext().preloadCache.get(targetUrl.toString());
604
734
  }
735
+ /**
736
+ * Stores the response of a preloaded request.
737
+ */
605
738
  function storePreloadRequest(targetUrl, response) {
606
739
  getInternalRouterContext().preloadCache.set(targetUrl.toString(), response);
607
740
  }
741
+ /**
742
+ * Discards a preloaded request.
743
+ */
608
744
  function discardPreloadedRequest(targetUrl) {
609
745
  return getInternalRouterContext().preloadCache.delete(targetUrl.toString());
610
746
  }
747
+ /** Preloads a hybrid request. */
611
748
  async function performPreloadRequest(options) {
612
749
  const context = getRouterContext();
613
750
  const url = makeUrl(options.url ?? context.url);
@@ -637,6 +774,17 @@ async function performPreloadRequest(options) {
637
774
  return false;
638
775
  }
639
776
  }
777
+ //#endregion
778
+ //#region src/router/router.ts
779
+ /**
780
+ * The hybridly router.
781
+ * This is the core function that you can use to navigate in
782
+ * your application. Make sure the routes you call return a
783
+ * hybrid response, otherwise you need to call `external`.
784
+ *
785
+ * @example
786
+ * router.get('/posts/edit', { post })
787
+ */
640
788
  const router = {
641
789
  abort: async () => getRouterContext().pendingNavigation?.controller.abort(),
642
790
  active: () => !!getRouterContext().pendingNavigation,
@@ -700,10 +848,12 @@ const router = {
700
848
  remember: (key, value) => remember(key, value)
701
849
  }
702
850
  };
851
+ /** Creates the hybridly router. */
703
852
  async function createRouter(options) {
704
853
  await initializeContext(options);
705
854
  return await initializeRouter();
706
855
  }
856
+ /** Performs every action necessary to make a hybrid navigation. */
707
857
  async function performHybridNavigation(options) {
708
858
  const navigationId = random();
709
859
  const context = getRouterContext();
@@ -854,9 +1004,14 @@ async function performHybridNavigation(options) {
854
1004
  if (context.pendingNavigation?.id === navigationId) setContext({ pendingNavigation: void 0 });
855
1005
  }
856
1006
  }
1007
+ /** Checks if the response contains a hybrid header. */
857
1008
  function isHybridResponse(response) {
858
1009
  return !!response?.headers[HYBRIDLY_HEADER];
859
1010
  }
1011
+ /**
1012
+ * Makes an internal navigation that swaps the view and updates the context.
1013
+ * @internal
1014
+ */
860
1015
  async function navigate(options) {
861
1016
  const context = getRouterContext();
862
1017
  options.hasDialog ??= !!options.payload?.dialog;
@@ -966,6 +1121,7 @@ async function performHybridRequest(targetUrl, options, abortController) {
966
1121
  }
967
1122
  });
968
1123
  }
1124
+ /** Initializes the router by reading the context and registering events if necessary. */
969
1125
  async function initializeRouter() {
970
1126
  const context = getRouterContext();
971
1127
  if (isBackForwardNavigation()) handleBackForwardNavigation();
@@ -983,6 +1139,7 @@ async function initializeRouter() {
983
1139
  await runHooks("ready", {}, context);
984
1140
  return context;
985
1141
  }
1142
+ /** Performs a local navigation to the given component without a round-trip. */
986
1143
  async function performLocalNavigation(targetUrl, options) {
987
1144
  const context = getRouterContext();
988
1145
  const url = normalizeUrl(targetUrl);
@@ -1001,7 +1158,14 @@ async function performLocalNavigation(targetUrl, options) {
1001
1158
  }
1002
1159
  });
1003
1160
  }
1161
+ //#endregion
1162
+ //#region src/authorization.ts
1163
+ /**
1164
+ * Checks whether the given data has the authorization for the given action.
1165
+ * If the data object has no authorization definition corresponding to the given action, this method will return `false`.
1166
+ */
1004
1167
  function can(resource, action) {
1005
1168
  return resource.authorization?.[action] ?? false;
1006
1169
  }
1170
+ //#endregion
1007
1171
  export { can, constants_exports as constants, createRouter, definePlugin, getRouterContext, makeUrl, registerHook, route, router, sameUrls };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hybridly/core",
3
3
  "type": "module",
4
- "version": "0.10.0-beta.12",
4
+ "version": "0.10.0-beta.13",
5
5
  "description": "Core functionality of Hybridly",
6
6
  "author": "Enzo Innocenzi <enzo@innocenzi.dev>",
7
7
  "license": "MIT",
@@ -43,7 +43,7 @@
43
43
  "axios": "^1.7.2"
44
44
  },
45
45
  "dependencies": {
46
- "@hybridly/utils": "0.10.0-beta.12",
46
+ "@hybridly/utils": "0.10.0-beta.13",
47
47
  "qs": "^6.14.0",
48
48
  "superjson": "^2.2.2"
49
49
  },