@qwik.dev/router 2.0.0-beta.3 → 2.0.0-beta.5

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/lib/index.d.ts CHANGED
@@ -19,6 +19,7 @@ import { RequestEventCommon } from '@qwik.dev/router/middleware/request-handler'
19
19
  import { RequestEventLoader } from '@qwik.dev/router/middleware/request-handler';
20
20
  import { RequestHandler } from '@qwik.dev/router/middleware/request-handler';
21
21
  import type { ResolveSyncValue } from '@qwik.dev/router/middleware/request-handler';
22
+ import type { SerializationStrategy } from '@qwik.dev/core/internal';
22
23
  import type * as v from 'valibot';
23
24
  import type { ValueOrPromise } from '@qwik.dev/core';
24
25
  import { z } from 'zod';
@@ -450,7 +451,9 @@ declare type LoaderConstructorQRL = {
450
451
 
451
452
  /** @public */
452
453
  declare type LoaderOptions = {
453
- id?: string;
454
+ readonly id?: string;
455
+ readonly validation?: DataValidator[];
456
+ readonly serializationStrategy?: SerializationStrategy;
454
457
  };
455
458
 
456
459
  /** @public */
@@ -575,7 +578,7 @@ export declare interface QwikRouterProps {
575
578
  *
576
579
  * @see https://github.com/WICG/view-transitions/blob/main/explainer.md
577
580
  * @see https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API
578
- * @see https://caniuse.com/mdn-api_viewtransition
581
+ * @see https://caniuse.com/mdn_api_viewtransition
579
582
  */
580
583
  viewTransition?: boolean;
581
584
  }
@@ -39,100 +39,19 @@ const ErrorBoundary = core.component$((props) => {
39
39
  const MODULE_CACHE = /* @__PURE__ */ new WeakMap();
40
40
  const CLIENT_DATA_CACHE = /* @__PURE__ */ new Map();
41
41
  const QACTION_KEY = "qaction";
42
+ const QLOADER_KEY = "qloaders";
42
43
  const QFN_KEY = "qfunc";
43
44
  const QDATA_KEY = "qdata";
44
- const toPath = (url) => url.pathname + url.search + url.hash;
45
- const toUrl = (url, baseUrl) => new URL(url, baseUrl.href);
46
- const isSameOrigin = (a, b) => a.origin === b.origin;
47
- const withSlash = (path) => path.endsWith("/") ? path : path + "/";
48
- const isSamePathname = ({ pathname: a }, { pathname: b }) => {
49
- const lDiff = Math.abs(a.length - b.length);
50
- return lDiff === 0 ? a === b : lDiff === 1 && withSlash(a) === withSlash(b);
51
- };
52
- const isSameSearchQuery = (a, b) => a.search === b.search;
53
- const isSamePath = (a, b) => isSameSearchQuery(a, b) && isSamePathname(a, b);
54
- const getClientDataPath = (pathname, pageSearch, action) => {
55
- let search = pageSearch ?? "";
56
- if (action) {
57
- search += (search ? "&" : "?") + QACTION_KEY + "=" + encodeURIComponent(action.id);
58
- }
59
- return pathname + (pathname.endsWith("/") ? "" : "/") + "q-data.json" + search;
60
- };
61
- const getClientNavPath = (props, baseUrl) => {
62
- const href = props.href;
63
- if (typeof href === "string" && typeof props.target !== "string" && !props.reload) {
64
- try {
65
- const linkUrl = toUrl(href.trim(), baseUrl.url);
66
- const currentUrl = toUrl("", baseUrl.url);
67
- if (isSameOrigin(linkUrl, currentUrl)) {
68
- return toPath(linkUrl);
69
- }
70
- } catch (e) {
71
- console.error(e);
72
- }
73
- } else if (props.reload) {
74
- return toPath(toUrl("", baseUrl.url));
75
- }
76
- return null;
77
- };
78
- const shouldPreload = (clientNavPath, currentLoc) => {
79
- if (clientNavPath) {
80
- const prefetchUrl = toUrl(clientNavPath, currentLoc.url);
81
- const currentUrl = toUrl("", currentLoc.url);
82
- return !isSamePathname(prefetchUrl, currentUrl);
83
- }
84
- return false;
85
- };
86
- const isPromise = (value) => {
87
- return value && typeof value.then === "function";
88
- };
89
- const deepFreeze = (obj) => {
90
- if (obj == null) {
91
- return obj;
92
- }
93
- Object.getOwnPropertyNames(obj).forEach((prop) => {
94
- const value = obj[prop];
95
- if (value && typeof value === "object" && !Object.isFrozen(value)) {
96
- deepFreeze(value);
97
- }
98
- });
99
- return Object.freeze(obj);
100
- };
101
- const clientNavigate = (win, navType, fromURL, toURL, replaceState = false) => {
102
- if (navType !== "popstate") {
103
- const samePath = isSamePath(fromURL, toURL);
104
- const sameHash = fromURL.hash === toURL.hash;
105
- if (!samePath || !sameHash) {
106
- const newState = {
107
- _qRouterScroll: newScrollState()
108
- };
109
- if (replaceState) {
110
- win.history.replaceState(newState, "", toPath(toURL));
111
- } else {
112
- win.history.pushState(newState, "", toPath(toURL));
113
- }
114
- }
115
- }
116
- };
117
- const newScrollState = () => {
118
- return {
119
- x: 0,
120
- y: 0,
121
- w: 0,
122
- h: 0
123
- };
124
- };
125
- const prefetchSymbols = (path) => {
126
- if (core.isBrowser) {
127
- path = path.endsWith("/") ? path : path + "/";
128
- path = path.length > 1 && path.startsWith("/") ? path.slice(1) : path;
129
- preloader.p(path, 0.8);
130
- }
131
- };
132
- const loadClientData = async (url, element, opts) => {
45
+ const Q_ROUTE = "q:route";
46
+ const DEFAULT_LOADERS_SERIALIZATION_STRATEGY = globalThis.__DEFAULT_LOADERS_SERIALIZATION_STRATEGY__ || "never";
47
+ const MAX_Q_DATA_RETRY_COUNT = 3;
48
+ const loadClientData = async (url, element, opts, retryCount = 0) => {
133
49
  const pagePathname = url.pathname;
134
50
  const pageSearch = url.search;
135
- const clientDataPath = getClientDataPath(pagePathname, pageSearch, opts?.action);
51
+ const clientDataPath = getClientDataPath(pagePathname, pageSearch, {
52
+ actionId: opts?.action?.id,
53
+ loaderIds: opts?.loaderIds
54
+ });
136
55
  let qData;
137
56
  if (!opts?.action) {
138
57
  qData = CLIENT_DATA_CACHE.get(clientDataPath);
@@ -147,6 +66,10 @@ const loadClientData = async (url, element, opts) => {
147
66
  opts.action.data = void 0;
148
67
  }
149
68
  qData = fetch(clientDataPath, fetchOptions).then((rsp) => {
69
+ if (rsp.status === 404 && opts?.loaderIds && retryCount < MAX_Q_DATA_RETRY_COUNT) {
70
+ opts.loaderIds = void 0;
71
+ return loadClientData(url, element, opts, retryCount + 1);
72
+ }
150
73
  if (rsp.redirected) {
151
74
  const redirectedURL = new URL(rsp.url);
152
75
  const isQData = redirectedURL.pathname.endsWith("/q-data.json");
@@ -227,6 +150,115 @@ const getFetchOptions = (action, noCache) => {
227
150
  };
228
151
  }
229
152
  };
153
+ const toPath = (url) => url.pathname + url.search + url.hash;
154
+ const toUrl = (url, baseUrl) => new URL(url, baseUrl.href);
155
+ const isSameOrigin = (a, b) => a.origin === b.origin;
156
+ const withSlash = (path) => path.endsWith("/") ? path : path + "/";
157
+ const isSamePathname = ({ pathname: a }, { pathname: b }) => {
158
+ const lDiff = Math.abs(a.length - b.length);
159
+ return lDiff === 0 ? a === b : lDiff === 1 && withSlash(a) === withSlash(b);
160
+ };
161
+ const isSameSearchQuery = (a, b) => a.search === b.search;
162
+ const isSamePath = (a, b) => isSameSearchQuery(a, b) && isSamePathname(a, b);
163
+ const getClientDataPath = (pathname, pageSearch, options) => {
164
+ let search = pageSearch ?? "";
165
+ if (options?.actionId) {
166
+ search += (search ? "&" : "?") + QACTION_KEY + "=" + encodeURIComponent(options.actionId);
167
+ }
168
+ if (options?.loaderIds) {
169
+ for (const loaderId of options.loaderIds) {
170
+ search += (search ? "&" : "?") + QLOADER_KEY + "=" + encodeURIComponent(loaderId);
171
+ }
172
+ }
173
+ return pathname + (pathname.endsWith("/") ? "" : "/") + "q-data.json" + search;
174
+ };
175
+ const getClientNavPath = (props, baseUrl) => {
176
+ const href = props.href;
177
+ if (typeof href === "string" && typeof props.target !== "string" && !props.reload) {
178
+ try {
179
+ const linkUrl = toUrl(href.trim(), baseUrl.url);
180
+ const currentUrl = toUrl("", baseUrl.url);
181
+ if (isSameOrigin(linkUrl, currentUrl)) {
182
+ return toPath(linkUrl);
183
+ }
184
+ } catch (e) {
185
+ console.error(e);
186
+ }
187
+ } else if (props.reload) {
188
+ return toPath(toUrl("", baseUrl.url));
189
+ }
190
+ return null;
191
+ };
192
+ const shouldPreload = (clientNavPath, currentLoc) => {
193
+ if (clientNavPath) {
194
+ const prefetchUrl = toUrl(clientNavPath, currentLoc.url);
195
+ const currentUrl = toUrl("", currentLoc.url);
196
+ return !isSamePathname(prefetchUrl, currentUrl);
197
+ }
198
+ return false;
199
+ };
200
+ const isPromise = (value) => {
201
+ return value && typeof value.then === "function";
202
+ };
203
+ const deepFreeze = (obj) => {
204
+ if (obj == null) {
205
+ return obj;
206
+ }
207
+ Object.getOwnPropertyNames(obj).forEach((prop) => {
208
+ const value = obj[prop];
209
+ if (value && typeof value === "object" && !Object.isFrozen(value)) {
210
+ deepFreeze(value);
211
+ }
212
+ });
213
+ return Object.freeze(obj);
214
+ };
215
+ const createLoaderSignal = (loadersObject, loaderId, url, serializationStrategy, container) => {
216
+ return core.createAsyncComputed$(async () => {
217
+ if (core.isBrowser && loadersObject[loaderId] === internal._UNINITIALIZED) {
218
+ const data = await loadClientData(url, void 0, {
219
+ loaderIds: [
220
+ loaderId
221
+ ]
222
+ });
223
+ loadersObject[loaderId] = data?.loaders[loaderId] ?? internal._UNINITIALIZED;
224
+ }
225
+ return loadersObject[loaderId];
226
+ }, {
227
+ container,
228
+ serializationStrategy
229
+ });
230
+ };
231
+ const clientNavigate = (win, navType, fromURL, toURL, replaceState = false) => {
232
+ if (navType !== "popstate") {
233
+ const samePath = isSamePath(fromURL, toURL);
234
+ const sameHash = fromURL.hash === toURL.hash;
235
+ if (!samePath || !sameHash) {
236
+ const newState = {
237
+ _qRouterScroll: newScrollState()
238
+ };
239
+ if (replaceState) {
240
+ win.history.replaceState(newState, "", toPath(toURL));
241
+ } else {
242
+ win.history.pushState(newState, "", toPath(toURL));
243
+ }
244
+ }
245
+ }
246
+ };
247
+ const newScrollState = () => {
248
+ return {
249
+ x: 0,
250
+ y: 0,
251
+ w: 0,
252
+ h: 0
253
+ };
254
+ };
255
+ const prefetchSymbols = (path) => {
256
+ if (core.isBrowser) {
257
+ path = path.endsWith("/") ? path : path + "/";
258
+ path = path.length > 1 && path.startsWith("/") ? path.slice(1) : path;
259
+ preloader.p(path, 0.8);
260
+ }
261
+ };
230
262
  const RouteStateContext = /* @__PURE__ */ core.createContextId("qc-s");
231
263
  const ContentContext = /* @__PURE__ */ core.createContextId("qc-c");
232
264
  const ContentInternalContext = /* @__PURE__ */ core.createContextId("qc-ic");
@@ -278,7 +310,7 @@ const Link = core.component$((props) => {
278
310
  }
279
311
  }
280
312
  }) : void 0;
281
- const preventDefault = clientNavPath ? core.sync$((event, target) => {
313
+ const preventDefault = clientNavPath ? core.sync$((event) => {
282
314
  if (!(event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)) {
283
315
  event.preventDefault();
284
316
  }
@@ -508,18 +540,30 @@ function lastIndexOf(text, start, match, searchIdx, notFoundIdx) {
508
540
  }
509
541
  return idx > start ? idx : notFoundIdx;
510
542
  }
543
+ var RouteDataProp = /* @__PURE__ */ function(RouteDataProp2) {
544
+ RouteDataProp2[RouteDataProp2["RouteName"] = 0] = "RouteName";
545
+ RouteDataProp2[RouteDataProp2["Loaders"] = 1] = "Loaders";
546
+ RouteDataProp2[RouteDataProp2["OriginalPathname"] = 2] = "OriginalPathname";
547
+ RouteDataProp2[RouteDataProp2["RouteBundleNames"] = 3] = "RouteBundleNames";
548
+ return RouteDataProp2;
549
+ }({});
550
+ var MenuDataProp = /* @__PURE__ */ function(MenuDataProp2) {
551
+ MenuDataProp2[MenuDataProp2["Pathname"] = 0] = "Pathname";
552
+ MenuDataProp2[MenuDataProp2["MenuLoader"] = 1] = "MenuLoader";
553
+ return MenuDataProp2;
554
+ }({});
511
555
  const loadRoute = async (routes, menus, cacheModules, pathname) => {
512
556
  if (!Array.isArray(routes)) {
513
557
  return null;
514
558
  }
515
559
  for (const routeData of routes) {
516
- const routeName = routeData[0];
560
+ const routeName = routeData[RouteDataProp.RouteName];
517
561
  const params = matchRoute(routeName, pathname);
518
562
  if (!params) {
519
563
  continue;
520
564
  }
521
- const loaders = routeData[1];
522
- const routeBundleNames = routeData[3];
565
+ const loaders = routeData[RouteDataProp.Loaders];
566
+ const routeBundleNames = routeData[RouteDataProp.RouteBundleNames];
523
567
  const modules = new Array(loaders.length);
524
568
  const pendingLoads = [];
525
569
  loaders.forEach((moduleLoader, i) => {
@@ -564,9 +608,9 @@ const loadModule = (moduleLoader, pendingLoads, moduleSetter, cacheModules) => {
564
608
  const getMenuLoader = (menus, pathname) => {
565
609
  if (menus) {
566
610
  pathname = pathname.endsWith("/") ? pathname : pathname + "/";
567
- const menu = menus.find((m) => m[0] === pathname || pathname.startsWith(m[0] + (pathname.endsWith("/") ? "" : "/")));
611
+ const menu = menus.find((m) => m[MenuDataProp.Pathname] === pathname || pathname.startsWith(m[MenuDataProp.Pathname] + (pathname.endsWith("/") ? "" : "/")));
568
612
  if (menu) {
569
- return menu[1];
613
+ return menu[MenuDataProp.MenuLoader];
570
614
  }
571
615
  }
572
616
  };
@@ -758,8 +802,8 @@ const spaInit = core.event$((_, el) => {
758
802
  };
759
803
  win[scrollEnabled] = true;
760
804
  setTimeout(() => {
761
- addEventListener("popstate", win[initPopstate]);
762
- addEventListener("scroll", win[initScroll], {
805
+ win.addEventListener("popstate", win[initPopstate]);
806
+ win.addEventListener("scroll", win[initScroll], {
763
807
  passive: true
764
808
  });
765
809
  document.body.addEventListener("click", win[initAnchors]);
@@ -833,9 +877,23 @@ const QwikRouterProvider = core.component$((props) => {
833
877
  deep: false
834
878
  });
835
879
  const navResolver = {};
836
- const loaderState = internal._weakSerialize(core.useStore(env.response.loaders, {
837
- deep: false
838
- }));
880
+ const container = internal._getContextContainer();
881
+ const getSerializationStrategy = (loaderId) => {
882
+ return env.response.loadersSerializationStrategy.get(loaderId) || DEFAULT_LOADERS_SERIALIZATION_STRATEGY;
883
+ };
884
+ const loadersObject = {};
885
+ const loaderState = {};
886
+ for (const [key, value] of Object.entries(env.response.loaders)) {
887
+ loadersObject[key] = value;
888
+ loaderState[key] = createLoaderSignal(loadersObject, key, url, getSerializationStrategy(key), container);
889
+ }
890
+ loadersObject[internal.SerializerSymbol] = (obj) => {
891
+ const loadersSerializationObject = {};
892
+ for (const [k, v] of Object.entries(obj)) {
893
+ loadersSerializationObject[k] = getSerializationStrategy(k) === "always" ? v : internal._UNINITIALIZED;
894
+ }
895
+ return loadersSerializationObject;
896
+ };
839
897
  const routeInternal = core.useSignal({
840
898
  type: "initial",
841
899
  dest: url,
@@ -928,7 +986,7 @@ const QwikRouterProvider = core.component$((props) => {
928
986
  let scroller = document.getElementById(QWIK_ROUTER_SCROLLER);
929
987
  if (!scroller) {
930
988
  scroller = document.getElementById(QWIK_CITY_SCROLLER);
931
- if (scroller) {
989
+ if (scroller && core.isDev) {
932
990
  console.warn(`Please update your scroller ID to "${QWIK_ROUTER_SCROLLER}" as "${QWIK_CITY_SCROLLER}" is deprecated and will be removed in V3`);
933
991
  }
934
992
  }
@@ -1069,11 +1127,21 @@ const QwikRouterProvider = core.component$((props) => {
1069
1127
  document.__q_scroll_restore__ = () => restoreScroll(navType, trackUrl, prevUrl, scroller, scrollState);
1070
1128
  }
1071
1129
  const loaders = clientPageData?.loaders;
1072
- const win = window;
1073
1130
  if (loaders) {
1074
- Object.assign(loaderState, loaders);
1131
+ const container2 = internal._getContextContainer();
1132
+ for (const [key, value] of Object.entries(loaders)) {
1133
+ const signal = loaderState[key];
1134
+ const awaitedValue = await value;
1135
+ loadersObject[key] = awaitedValue;
1136
+ if (!signal) {
1137
+ loaderState[key] = createLoaderSignal(loadersObject, key, trackUrl, DEFAULT_LOADERS_SERIALIZATION_STRATEGY, container2);
1138
+ } else {
1139
+ signal.invalidate();
1140
+ }
1141
+ }
1075
1142
  }
1076
1143
  CLIENT_DATA_CACHE.clear();
1144
+ const win = window;
1077
1145
  if (!win._qRouterSPA) {
1078
1146
  win._qRouterSPA = true;
1079
1147
  history.scrollRestoration = "manual";
@@ -1203,8 +1271,8 @@ const QwikRouterProvider = core.component$((props) => {
1203
1271
  }
1204
1272
  };
1205
1273
  _waitNextPage().then(() => {
1206
- const container = internal._getQContainerElement(elm);
1207
- container.setAttribute("q:route", routeName);
1274
+ const container2 = internal._getQContainerElement(elm);
1275
+ container2.setAttribute(Q_ROUTE, routeName);
1208
1276
  const scrollState2 = currentScrollState(scroller);
1209
1277
  saveScrollHistory(scrollState2);
1210
1278
  win._qRouterScrollEnabled = true;
@@ -1237,7 +1305,7 @@ const QwikRouterMockProvider = core.component$((props) => {
1237
1305
  }, {
1238
1306
  deep: false
1239
1307
  });
1240
- const loaderState = core.useSignal({});
1308
+ const loaderState = {};
1241
1309
  const routeInternal = core.useSignal({
1242
1310
  type: "initial",
1243
1311
  dest: url
@@ -1482,24 +1550,26 @@ const globalActionQrl = (actionQrl, ...rest) => {
1482
1550
  const routeAction$ = /* @__PURE__ */ core.implicit$FirstArg(routeActionQrl);
1483
1551
  const globalAction$ = /* @__PURE__ */ core.implicit$FirstArg(globalActionQrl);
1484
1552
  const routeLoaderQrl = (loaderQrl, ...rest) => {
1485
- const { id, validators } = getValidators(rest, loaderQrl);
1553
+ const { id, validators, serializationStrategy } = getValidators(rest, loaderQrl);
1486
1554
  function loader() {
1487
- return core.useContext(RouteStateContext, (state) => {
1488
- if (!(id in state)) {
1489
- throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared.
1555
+ const iCtx = internal._useInvokeContext();
1556
+ const state = iCtx.$container$.resolveContext(iCtx.$hostElement$, RouteStateContext);
1557
+ if (!(id in state)) {
1558
+ throw new Error(`routeLoader$ "${loaderQrl.getSymbol()}" was invoked in a route where it was not declared.
1490
1559
  This is because the routeLoader$ was not exported in a 'layout.tsx' or 'index.tsx' file of the existing route.
1491
1560
  For more information check: https://qwik.dev/docs/route-loader/
1492
1561
 
1493
1562
  If your are managing reusable logic or a library it is essential that this function is re-exported from within 'layout.tsx' or 'index.tsx file of the existing route otherwise it will not run or throw exception.
1494
1563
  For more information check: https://qwik.dev/docs/re-exporting-loaders/`);
1495
- }
1496
- return internal._wrapStore(state, id);
1497
- });
1564
+ }
1565
+ core.untrack(() => state[id].value);
1566
+ return state[id];
1498
1567
  }
1499
1568
  loader.__brand = "server_loader";
1500
1569
  loader.__qrl = loaderQrl;
1501
1570
  loader.__validators = validators;
1502
1571
  loader.__id = id;
1572
+ loader.__serializationStrategy = serializationStrategy;
1503
1573
  Object.freeze(loader);
1504
1574
  return loader;
1505
1575
  };
@@ -1728,6 +1798,7 @@ const serverQrl = (qrl, options) => {
1728
1798
  const server$ = /* @__PURE__ */ core.implicit$FirstArg(serverQrl);
1729
1799
  const getValidators = (rest, qrl) => {
1730
1800
  let id;
1801
+ let serializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY;
1731
1802
  const validators = [];
1732
1803
  if (rest.length === 1) {
1733
1804
  const options = rest[0];
@@ -1736,6 +1807,9 @@ const getValidators = (rest, qrl) => {
1736
1807
  validators.push(options);
1737
1808
  } else {
1738
1809
  id = options.id;
1810
+ if (options.serializationStrategy) {
1811
+ serializationStrategy = options.serializationStrategy;
1812
+ }
1739
1813
  if (options.validation) {
1740
1814
  validators.push(...options.validation);
1741
1815
  }
@@ -1756,7 +1830,8 @@ const getValidators = (rest, qrl) => {
1756
1830
  }
1757
1831
  return {
1758
1832
  validators: validators.reverse(),
1759
- id
1833
+ id,
1834
+ serializationStrategy
1760
1835
  };
1761
1836
  };
1762
1837
  const deserializeStream = async function* (stream, ctxElm, abortSignal) {