@qwik.dev/router 2.0.0-beta.38 → 2.0.0-beta.39

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,7 +1,7 @@
1
1
  import { e as ensureSlash } from './pathname.mjs';
2
2
  import { createContextId, isServer as isServer$1, implicit$FirstArg, isDev, createComputedQrl, inlinedQrl, _captures, withLocale } from '@qwik.dev/core';
3
3
  import * as qwikRouterConfig from '@qwik-router-config';
4
- import { _getAsyncLocalStorage, _getContextEvent, _resolveContextWithoutSequentialScope, _injectAsyncSignalValue, _UNINITIALIZED, _markSignalAsExternallyOwned, _deserialize, SerializerSymbol, _verifySerializable } from '@qwik.dev/core/internal';
4
+ import { _getAsyncLocalStorage, _getContextEvent, _resolveContextWithoutSequentialScope, _injectAsyncSignalValue, _UNINITIALIZED, _deserialize, _markSignalAsExternallyOwned, SerializerSymbol, _verifySerializable } from '@qwik.dev/core/internal';
5
5
  import { isServer } from '@qwik.dev/core/build';
6
6
  import { g as getLoaderName } from './request-path.mjs';
7
7
  import { R as RedirectMessage, S as ServerError, t as throwIfControlFlowSignal } from './server-error.mjs';
@@ -578,6 +578,7 @@ const RouteActionContext = /* @__PURE__ */ createContextId("qr-a");
578
578
  const RoutePreventNavigateContext = /* @__PURE__ */ createContextId("qr-p");
579
579
  const HttpStatusContext = /* @__PURE__ */ createContextId("qr-hs");
580
580
 
581
+ const REQUEST_ROUTE_LOADER_STATE = "@routeLoaderState";
581
582
  const REQUEST_LOADER_PATHS_STORE = "@loaderPathsStore";
582
583
  const REQUEST_ROUTE_LOADERS = "@routeLoaders";
583
584
  const REQUEST_ROUTE_LOADER_PROMISES = "@routeLoaderPromises";
@@ -586,20 +587,9 @@ const REQUEST_ROUTE_LOADER_ROOT_EVENT = "@routeLoaderRootEvent";
586
587
  const ROUTE_LOADER_VALUE_PREFIX = "__qwik_route_loader_value__";
587
588
  const FULLPATH_HEADER = "X-Qwik-fullpath";
588
589
  const ROUTE_PATH_HEADER = "X-Qwik-route-path";
589
- const LOADER_FETCH_CACHE_TTL = 5e3;
590
- const LOADER_FETCH_CACHE_MAX = 128;
591
- const fetchCache = /* @__PURE__ */ new Map();
592
- const perfNow = () => globalThis.performance?.now() ?? Date.now();
590
+ const navFetchCache = /* @__PURE__ */ new Map();
591
+ const clearNavFetchCache = () => navFetchCache.clear();
593
592
  const isRedirectStatus = (status) => status >= 300 && status < 400;
594
- const setCache = (key, entry) => {
595
- fetchCache.set(key, entry);
596
- if (fetchCache.size > LOADER_FETCH_CACHE_MAX) {
597
- const oldestKey = fetchCache.keys().next().value;
598
- if (oldestKey !== void 0) {
599
- fetchCache.delete(oldestKey);
600
- }
601
- }
602
- };
603
593
  const wrapWithAbort = (promise, signal) => {
604
594
  if (signal.aborted) {
605
595
  return Promise.reject(signal.reason);
@@ -639,15 +629,9 @@ const fetchRouteLoaderData = async (loaderId, routePath, manifestHash, opts) =>
639
629
  const cacheKey = `${url}
640
630
  ${headers[FULLPATH_HEADER] ?? ""}`;
641
631
  if (!opts?.ignoreCache) {
642
- const entry = fetchCache.get(cacheKey);
632
+ const entry = navFetchCache.get(cacheKey);
643
633
  if (entry) {
644
- if (entry.promise) {
645
- return opts?.signal ? wrapWithAbort(entry.promise, opts.signal) : entry.promise;
646
- }
647
- if (entry.expires > perfNow()) {
648
- return entry.value;
649
- }
650
- fetchCache.delete(cacheKey);
634
+ return opts?.signal ? wrapWithAbort(entry, opts.signal) : entry;
651
635
  }
652
636
  }
653
637
  const request = async () => {
@@ -672,41 +656,31 @@ ${headers[FULLPATH_HEADER] ?? ""}`;
672
656
  if (!response.ok) {
673
657
  return void 0;
674
658
  }
675
- const text = await response.text();
676
- return await _deserialize(text) ?? void 0;
659
+ return {
660
+ raw: await response.text()
661
+ };
677
662
  };
678
663
  if (opts?.ignoreCache) {
679
664
  return request();
680
665
  }
681
666
  if (opts?.signal) {
682
667
  return request().then((value) => {
683
- if (value !== void 0 && !fetchCache.has(cacheKey)) {
684
- setCache(cacheKey, {
685
- value,
686
- expires: perfNow() + LOADER_FETCH_CACHE_TTL
687
- });
668
+ if (value !== void 0 && !navFetchCache.has(cacheKey)) {
669
+ navFetchCache.set(cacheKey, Promise.resolve(value));
688
670
  }
689
671
  return value;
690
672
  });
691
673
  }
692
674
  const promise = request().then((value) => {
693
675
  if (value === void 0) {
694
- fetchCache.delete(cacheKey);
695
- } else {
696
- setCache(cacheKey, {
697
- value,
698
- expires: perfNow() + LOADER_FETCH_CACHE_TTL
699
- });
676
+ navFetchCache.delete(cacheKey);
700
677
  }
701
678
  return value;
702
679
  }, (err) => {
703
- fetchCache.delete(cacheKey);
680
+ navFetchCache.delete(cacheKey);
704
681
  throw err;
705
682
  });
706
- setCache(cacheKey, {
707
- promise,
708
- expires: perfNow() + LOADER_FETCH_CACHE_TTL
709
- });
683
+ navFetchCache.set(cacheKey, promise);
710
684
  return promise;
711
685
  };
712
686
  const filterSearchParams = (params, allowed) => {
@@ -724,12 +698,10 @@ const filterSearchParams = (params, allowed) => {
724
698
  const getLoaderOptions = (rest) => {
725
699
  let id;
726
700
  let serializationStrategy = DEFAULT_LOADERS_SERIALIZATION_STRATEGY;
727
- let expires;
728
- let poll;
701
+ let cacheControl;
729
702
  let eTag;
730
703
  let cacheKey;
731
704
  let search;
732
- let allowStale = true;
733
705
  let blockSSR = true;
734
706
  const validators = [];
735
707
  if (rest.length === 1) {
@@ -747,11 +719,8 @@ const getLoaderOptions = (rest) => {
747
719
  if (options.validation) {
748
720
  validators.push(...options.validation);
749
721
  }
750
- if ("expires" in options) {
751
- expires = options.expires;
752
- }
753
- if ("poll" in options) {
754
- poll = options.poll;
722
+ if ("cacheControl" in options) {
723
+ cacheControl = options.cacheControl;
755
724
  }
756
725
  if ("eTag" in options) {
757
726
  eTag = options.eTag;
@@ -764,9 +733,6 @@ const getLoaderOptions = (rest) => {
764
733
  } else if (globalThis.__STRICT_LOADERS__) {
765
734
  search = [];
766
735
  }
767
- if (options.allowStale === false) {
768
- allowStale = false;
769
- }
770
736
  if (options.blockSSR === false) {
771
737
  if (!__EXPERIMENTAL__.blockSSR) {
772
738
  throw new Error('`blockSSR: false` is an experimental feature and is not enabled. Please enable the feature flag by adding `experimental: ["blockSSR"]` to your qwikVite plugin options.');
@@ -782,12 +748,10 @@ const getLoaderOptions = (rest) => {
782
748
  id,
783
749
  validators: validators.reverse(),
784
750
  serializationStrategy,
785
- expires,
786
- poll,
751
+ cacheControl,
787
752
  eTag,
788
753
  cacheKey,
789
754
  search,
790
- allowStale,
791
755
  blockSSR
792
756
  };
793
757
  };
@@ -817,6 +781,14 @@ function getRouteLoaderPromises(requestEv) {
817
781
  }
818
782
  return promises;
819
783
  }
784
+ function clearRouteLoaderData(requestEv) {
785
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADER_STATE);
786
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADER_VALUES);
787
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADER_PROMISES);
788
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADER_EVENTS);
789
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADER_ROOT_EVENT);
790
+ requestEv.sharedMap.delete(REQUEST_ROUTE_LOADERS);
791
+ }
820
792
  function setRouteLoaders(requestEv, loaders) {
821
793
  requestEv.sharedMap.set(REQUEST_ROUTE_LOADERS, loaders);
822
794
  }
@@ -875,6 +847,15 @@ const getModuleRouteLoaders = (mods) => {
875
847
  }
876
848
  return routeLoaders;
877
849
  };
850
+ const immutableLoaderIds = /* @__PURE__ */ new Set();
851
+ const isImmutableLoader = (loaderId) => immutableLoaderIds.has(loaderId);
852
+ const invalidateNavRouteLoaders = (state) => {
853
+ for (const id in state) {
854
+ if (!id.startsWith(ROUTE_LOADER_VALUE_PREFIX) && !immutableLoaderIds.has(id)) {
855
+ state[id].invalidate();
856
+ }
857
+ }
858
+ };
878
859
  const setLoaderSignalValue = (signal, value) => {
879
860
  _injectAsyncSignalValue(signal, value);
880
861
  };
@@ -907,7 +888,16 @@ ${filteredSearch}`;
907
888
  if (!loaderRequestEv) {
908
889
  url.pathname = pathname;
909
890
  url.search = filteredSearch;
891
+ const request = new Request(url, rootRequestEv.request);
910
892
  loaderRequestEv = Object.create(rootRequestEv, {
893
+ originalUrl: {
894
+ value: new URL(url),
895
+ enumerable: true
896
+ },
897
+ params: {
898
+ value: {},
899
+ enumerable: true
900
+ },
911
901
  pathname: {
912
902
  get: () => url.pathname,
913
903
  enumerable: true
@@ -916,6 +906,10 @@ ${filteredSearch}`;
916
906
  get: () => url.searchParams,
917
907
  enumerable: true
918
908
  },
909
+ request: {
910
+ value: request,
911
+ enumerable: true
912
+ },
919
913
  url: {
920
914
  value: url,
921
915
  enumerable: true
@@ -1082,6 +1076,7 @@ const createRouteLoaderSignal = (loader, routeLoaderCtx, state) => {
1082
1076
  if (!isServer$1 && resumeValueKey2 in stateValues2) {
1083
1077
  stateValues2[resumeValueKey2] = value;
1084
1078
  }
1079
+ lastFetch2.raw = void 0;
1085
1080
  return value;
1086
1081
  }
1087
1082
  if (isServer$1) {
@@ -1099,24 +1094,30 @@ const createRouteLoaderSignal = (loader, routeLoaderCtx, state) => {
1099
1094
  }
1100
1095
  let fetchUrl = pageUrl;
1101
1096
  if (searchFilter2) {
1102
- const filteredSearch = filterSearchParams(pageUrl.searchParams, searchFilter2);
1103
- if (ctx.previous !== void 0 && filteredSearch === lastFetch2.filteredSearch && fetchRoutePath === lastFetch2.routePath) {
1104
- return ctx.previous;
1105
- }
1106
- lastFetch2.filteredSearch = filteredSearch;
1107
- lastFetch2.routePath = fetchRoutePath;
1108
1097
  fetchUrl = new URL(pageUrl.href);
1109
- fetchUrl.search = filteredSearch;
1098
+ fetchUrl.search = filterSearchParams(pageUrl.searchParams, searchFilter2);
1110
1099
  }
1111
- const response = await fetchRouteLoaderData(id2, fetchRoutePath, mHash, {
1100
+ const result = await fetchRouteLoaderData(id2, fetchRoutePath, mHash, {
1112
1101
  pageUrl: fetchUrl,
1113
1102
  basePath,
1114
1103
  ignoreCache: ctx.info === true,
1115
1104
  signal: ctx.abortSignal
1116
1105
  });
1117
- if (!response) {
1106
+ if (!result) {
1118
1107
  throw new Error(`Loader ${id2} returned empty response`);
1119
1108
  }
1109
+ let response;
1110
+ if (result.raw === void 0) {
1111
+ response = result;
1112
+ } else {
1113
+ if (result.raw === lastFetch2.raw && ctx.previous !== void 0) {
1114
+ return ctx.previous;
1115
+ }
1116
+ response = await _deserialize(result.raw);
1117
+ if (!response) {
1118
+ throw new Error(`Loader ${id2} returned empty response`);
1119
+ }
1120
+ }
1120
1121
  if (response.r) {
1121
1122
  const goto = routeLoaderCtx2.goto;
1122
1123
  if (goto) {
@@ -1131,6 +1132,7 @@ const createRouteLoaderSignal = (loader, routeLoaderCtx, state) => {
1131
1132
  if (response.e) {
1132
1133
  throw response.e;
1133
1134
  }
1135
+ lastFetch2.raw = result.raw;
1134
1136
  if (needsResumeFetch) {
1135
1137
  stateValues2[resumeValueKey2] = response.d;
1136
1138
  }
@@ -1144,15 +1146,15 @@ const createRouteLoaderSignal = (loader, routeLoaderCtx, state) => {
1144
1146
  searchFilter,
1145
1147
  stateValues
1146
1148
  ]), {
1147
- serializationStrategy: loader.__serializationStrategy,
1148
- expires: loader.__expires,
1149
- poll: loader.__poll,
1150
- allowStale: loader.__allowStale
1149
+ serializationStrategy: loader.__serializationStrategy
1151
1150
  });
1152
1151
  _markSignalAsExternallyOwned(signal);
1153
1152
  return signal;
1154
1153
  };
1155
1154
  const ensureRouteLoaderSignal = (loader, state, routeLoaderCtx) => {
1155
+ if (loader.__cacheControl === "immutable") {
1156
+ immutableLoaderIds.add(loader.__id);
1157
+ }
1156
1158
  if (isServer$1 && loader.__serializationStrategy === "never") {
1157
1159
  state[getRouteLoaderValueStateKey(loader.__id)] = _UNINITIALIZED;
1158
1160
  }
@@ -1205,7 +1207,7 @@ const getRouteLoaderResponse = async (loaderQrl, validators, requestEv) => {
1205
1207
  }
1206
1208
  };
1207
1209
  const routeLoaderQrl = (loaderQrl, ...rest) => {
1208
- const { id, validators, serializationStrategy, expires, poll, eTag, cacheKey, search, allowStale, blockSSR } = getLoaderOptions(rest);
1210
+ const { id, validators, serializationStrategy, cacheControl, eTag, cacheKey, search, blockSSR } = getLoaderOptions(rest);
1209
1211
  function loader() {
1210
1212
  const state = _resolveContextWithoutSequentialScope(RouteStateContext);
1211
1213
  let signal = state[loader.__id];
@@ -1221,12 +1223,10 @@ const routeLoaderQrl = (loaderQrl, ...rest) => {
1221
1223
  loader.__validators = validators;
1222
1224
  loader.__id = id ?? loaderQrl.getHash();
1223
1225
  loader.__serializationStrategy = serializationStrategy;
1224
- loader.__expires = expires ?? 12e4;
1225
- loader.__poll = poll ?? false;
1226
+ loader.__cacheControl = cacheControl;
1226
1227
  loader.__eTag = eTag;
1227
1228
  loader.__cacheKey = cacheKey;
1228
1229
  loader.__search = search;
1229
- loader.__allowStale = allowStale;
1230
1230
  loader.__blockSSR = blockSSR;
1231
1231
  Object.freeze(loader);
1232
1232
  return loader;
@@ -1422,4 +1422,4 @@ const resolveHead = (actionData, loaderState, routeLocation, contentModules, loc
1422
1422
  return resolveRouteConfig(getData, routeLocation, contentModules, locale, actionData?.status ?? 200, defaults).head;
1423
1423
  };
1424
1424
 
1425
- export { getRouteLoaders as A, getRouteLoaderValues as B, ContentContext as C, DocumentHeadContext as D, loadRouteLoader as E, FULLPATH_HEADER as F, isPromise as G, HttpStatusContext as H, getLoaderRequestEvent as I, resolveRouteLoaderByHash as J, getRouteLoaderResponse as K, getRouteLoaderCtx as L, setRouteLoaders as M, resolveRouteConfig as N, matchesRouteLoaderId as O, QACTION_KEY as Q, ROUTE_PATH_HEADER as R, _asyncRequestStore as _, isSameOrigin as a, setLoaderSignalValue as b, createDocumentHead as c, ContentInternalContext as d, ensureRouteLoaderSignals as e, fetchRouteLoaderData as f, getClientNavPath as g, RouteLocationContext as h, isSamePath as i, RouteNavigateContext as j, RouteStateContext as k, loadRoute as l, RouteLoaderCtxContext as m, RouteActionContext as n, RoutePreventNavigateContext as o, Q_ROUTE as p, toUrl as q, resolveHead as r, shouldPreload as s, toPath as t, updateRouteLoaderPaths as u, getRequestEvent as v, QFN_KEY as w, QDATA_KEY as x, routeLoader$ as y, routeLoaderQrl as z };
1425
+ export { QFN_KEY as A, QDATA_KEY as B, ContentContext as C, DocumentHeadContext as D, routeLoader$ as E, FULLPATH_HEADER as F, routeLoaderQrl as G, HttpStatusContext as H, getRouteLoaders as I, getRouteLoaderValues as J, loadRouteLoader as K, isPromise as L, getLoaderRequestEvent as M, resolveRouteLoaderByHash as N, getRouteLoaderResponse as O, getRouteLoaderCtx as P, QACTION_KEY as Q, ROUTE_PATH_HEADER as R, setRouteLoaders as S, resolveRouteConfig as T, clearRouteLoaderData as U, matchesRouteLoaderId as V, _asyncRequestStore as _, getClientNavPath as a, isSameOrigin as b, setLoaderSignalValue as c, createDocumentHead as d, ensureRouteLoaderSignals as e, fetchRouteLoaderData as f, getModuleRouteLoaders as g, ContentInternalContext as h, isSamePath as i, RouteLocationContext as j, RouteNavigateContext as k, loadRoute as l, RouteStateContext as m, RouteLoaderCtxContext as n, RouteActionContext as o, RoutePreventNavigateContext as p, invalidateNavRouteLoaders as q, isImmutableLoader as r, shouldPreload as s, toPath as t, updateRouteLoaderPaths as u, clearNavFetchCache as v, resolveHead as w, Q_ROUTE as x, toUrl as y, getRequestEvent as z };
@@ -1,5 +1,5 @@
1
1
  import { useContext, implicit$FirstArg, useVisibleTaskQrl, inlinedQrl, _captures, noSerialize, useServerData } from '@qwik.dev/core';
2
- import { C as ContentContext, D as DocumentHeadContext, H as HttpStatusContext, h as RouteLocationContext, j as RouteNavigateContext, o as RoutePreventNavigateContext, n as RouteActionContext } from './head.qwik.mjs';
2
+ import { C as ContentContext, D as DocumentHeadContext, H as HttpStatusContext, j as RouteLocationContext, k as RouteNavigateContext, p as RoutePreventNavigateContext, o as RouteActionContext } from './head.qwik.mjs';
3
3
 
4
4
  const useHttpStatus = () => useContext(HttpStatusContext).value;
5
5
  const useContent = () => useContext(ContentContext);
@@ -1,6 +1,6 @@
1
1
  import { isDev as isDev$1, inlinedQrl } from '@qwik.dev/core';
2
2
  import { isDev, _deserialize, _serialize, _verifySerializable } from '@qwik.dev/core/internal';
3
- import { Q as QACTION_KEY, B as getRouteLoaderValues, E as loadRouteLoader, G as isPromise, x as QDATA_KEY, I as getLoaderRequestEvent, J as resolveRouteLoaderByHash, K as getRouteLoaderResponse, L as getRouteLoaderCtx, M as setRouteLoaders, F as FULLPATH_HEADER, p as Q_ROUTE, w as QFN_KEY, N as resolveRouteConfig, O as matchesRouteLoaderId, _ as _asyncRequestStore } from './head.qwik.mjs';
3
+ import { Q as QACTION_KEY, J as getRouteLoaderValues, K as loadRouteLoader, L as isPromise, B as QDATA_KEY, M as getLoaderRequestEvent, N as resolveRouteLoaderByHash, O as getRouteLoaderResponse, P as getRouteLoaderCtx, S as setRouteLoaders, F as FULLPATH_HEADER, x as Q_ROUTE, A as QFN_KEY, T as resolveRouteConfig, U as clearRouteLoaderData, V as matchesRouteLoaderId, _ as _asyncRequestStore } from './head.qwik.mjs';
4
4
  import { e as ensureSlash } from './pathname.mjs';
5
5
  import { r as recognizeRequest, t as trimRecognizedInternalPathname, Q as QLoaderId, c as IsQAction, d as QActionId, I as IsQLoader, a as resolveValidInternalFullPathname } from './request-path.mjs';
6
6
  import { A as AbortMessage, S as ServerError, R as RedirectMessage, t as throwIfControlFlowSignal } from './server-error.mjs';
@@ -737,6 +737,20 @@ function createLoaderRequestEventFactory(requestEv) {
737
737
  return (loader) => getLoaderRequestEvent(loader, requestEv);
738
738
  }
739
739
 
740
+ function resolveLoaderCacheControl(option, requestEv) {
741
+ const value = typeof option === "function" ? option(requestEv) : option;
742
+ return value === void 0 ? "no-cache" : value;
743
+ }
744
+ async function runBlockingLoadersBeforeTarget(routeLoaders, targetLoader, requestEv) {
745
+ for (const loader of routeLoaders) {
746
+ if (loader === targetLoader) {
747
+ return;
748
+ }
749
+ if (loader.__blockSSR) {
750
+ await loadRouteLoader(loader, requestEv);
751
+ }
752
+ }
753
+ }
740
754
  function setLoaderData(requestEv, routeLoaders, loaderPaths) {
741
755
  if (loaderPaths) {
742
756
  Object.assign(getRouteLoaderCtx(requestEv).loaderPaths, loaderPaths);
@@ -761,14 +775,11 @@ function addVaryHeader(requestEv, value) {
761
775
  requestEv.headers.set("Vary", `${vary}, ${value}`);
762
776
  }
763
777
  }
764
- async function sendLoaderResponse(requestEv, data, loader) {
778
+ async function sendLoaderResponse(requestEv, data, cacheControl) {
765
779
  requestEv.headers.set("Content-Type", "application/json; charset=utf-8");
766
780
  addVaryHeader(requestEv, FULLPATH_HEADER);
767
- if (loader?.__expires && loader.__expires > 0) {
768
- requestEv.cacheControl({
769
- maxAge: Math.ceil(loader.__expires / 1e3),
770
- private: true
771
- });
781
+ if (cacheControl !== null && !requestEv.headers.has("Cache-Control")) {
782
+ requestEv.cacheControl(cacheControl);
772
783
  }
773
784
  requestEv.send(200, data);
774
785
  }
@@ -790,7 +801,9 @@ function loaderHandler(routeLoaders, loaderPaths) {
790
801
  return;
791
802
  }
792
803
  setLoaderData(requestEv, routeLoaders, loaderPaths);
804
+ await runBlockingLoadersBeforeTarget(routeLoaders, loader, requestEv);
793
805
  const loaderRequestEv = createLoaderRequestEventFactory(requestEv)(loader);
806
+ const cacheControl = resolveLoaderCacheControl(loader.__cacheControl, loaderRequestEv);
794
807
  const normalizedETag = loader.__eTag !== void 0 ? resolvePreETag(loader.__eTag, loaderRequestEv) : "";
795
808
  if (normalizedETag && performETagMatch(loaderRequestEv, normalizedETag)) {
796
809
  return;
@@ -809,7 +822,7 @@ function loaderHandler(routeLoaders, loaderPaths) {
809
822
  } else {
810
823
  setETagHeader(loaderRequestEv, cached.eTag);
811
824
  }
812
- await sendLoaderResponse(requestEv, cached.body, loader);
825
+ await sendLoaderResponse(requestEv, cached.body, cacheControl);
813
826
  return;
814
827
  }
815
828
  }
@@ -827,7 +840,7 @@ function loaderHandler(routeLoaders, loaderPaths) {
827
840
  if (!normalizedETag && finalETag && performETagMatch(loaderRequestEv, finalETag)) {
828
841
  return;
829
842
  }
830
- await sendLoaderResponse(requestEv, data, loader);
843
+ await sendLoaderResponse(requestEv, data, cacheControl);
831
844
  };
832
845
  }
833
846
  async function sendJsonResponse(requestEv, responseData, status = 200) {
@@ -1157,6 +1170,9 @@ function createResolveRequestHandlers() {
1157
1170
  if (serverPlugins) {
1158
1171
  _resolveRequestHandlers(routeLoaders, routeActions, requestHandlers, serverPlugins, isPageRoute, method);
1159
1172
  }
1173
+ if (!route.$notFound$ && isPageRoute && (method === "POST" || method === "GET")) {
1174
+ requestHandlers.push(runServerFunction);
1175
+ }
1160
1176
  const routeModules = route.$mods$;
1161
1177
  _resolveRequestHandlers(routeLoaders, routeActions, requestHandlers, routeModules, isPageRoute, method, isPageRoute);
1162
1178
  const routeName = route.$routeName$;
@@ -1170,9 +1186,6 @@ function createResolveRequestHandlers() {
1170
1186
  if (isPageRoute) {
1171
1187
  requestHandlers.push(loaderHandler(routeLoaders, route.$loaderPaths$));
1172
1188
  requestHandlers.push(actionHandler(routeActions));
1173
- if (method === "POST" || method === "GET") {
1174
- requestHandlers.push(runServerFunction);
1175
- }
1176
1189
  if (!route.$notFound$) {
1177
1190
  requestHandlers.push(fixTrailingSlash2);
1178
1191
  }
@@ -1449,10 +1462,17 @@ function createResolveRequestHandlers() {
1449
1462
  await loadHttpError()
1450
1463
  ];
1451
1464
  requestEv.sharedMap.set(RequestEvHttpStatusMessage, typeof e.data === "string" ? e.data : "Server Error");
1465
+ clearErrorResponseData(requestEv);
1452
1466
  await renderHandler(requestEv);
1453
1467
  }
1454
1468
  };
1455
1469
  }
1470
+ function clearErrorResponseData(requestEv) {
1471
+ clearRouteLoaderData(requestEv);
1472
+ requestEv.sharedMap.delete(RequestEvSharedActionId);
1473
+ requestEv.sharedMap.delete(RequestEvSharedActionFormData);
1474
+ requestEv.sharedMap.delete("@actionResult");
1475
+ }
1456
1476
  async function runValidators(requestEv, validators, data) {
1457
1477
  let lastResult = {
1458
1478
  success: true,
@@ -2,7 +2,7 @@ import { _serialize } from '@qwik.dev/core/internal';
2
2
  import { parentPort } from 'node:worker_threads';
3
3
  import { r as runQwikRouter, a as resolveRequestHandlers, b as renderQwikMiddleware } from './user-response.mjs';
4
4
  import { b as trimInternalPathname } from './request-path.mjs';
5
- import { A as getRouteLoaders, B as getRouteLoaderValues, l as loadRoute } from './head.qwik.mjs';
5
+ import { I as getRouteLoaders, J as getRouteLoaderValues, l as loadRoute } from './head.qwik.mjs';
6
6
 
7
7
  const staticWorkerThreadDeps = {
8
8
  loadRoute,
@@ -136,7 +136,7 @@ async function workerRender(sys, opts, staticRoute, pendingPromises, callback, d
136
136
  const loaderValues = getRouteLoaderValues(requestEv);
137
137
  const manifestHash = opts.manifest?.manifestHash || "dev";
138
138
  for (const loader of routeLoaders) {
139
- if (loader.__expires !== 0) {
139
+ if (loader.__cacheControl !== "immutable") {
140
140
  continue;
141
141
  }
142
142
  const data = loaderValues[loader.__id];
package/lib/index.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference path="./modules.d.ts" />
2
2
 
3
3
  import type { AbortMessage } from '@qwik.dev/router/middleware/request-handler';
4
+ import type { CacheControl } from '@qwik.dev/router/middleware/request-handler';
4
5
  import { Component } from '@qwik.dev/core';
5
6
  import type { ComputedSignal } from '@qwik.dev/core';
6
7
  import { Cookie } from '@qwik.dev/router/middleware/request-handler';
@@ -594,19 +595,20 @@ declare type LoaderOptions = {
594
595
  readonly validation?: DataValidator[];
595
596
  readonly serializationStrategy?: SerializationStrategy;
596
597
  /**
597
- * Time in milliseconds after which the loader data is considered stale. The server derives
598
- * `Cache-Control: max-age` seconds from this value on loader responses.
598
+ * Cache-Control for this loader's JSON responses. The browser HTTP cache is the single source of
599
+ * freshness for loader data: on every client navigation the loaders re-fetch, and this header
600
+ * decides whether the browser serves the fetch from cache, revalidates, or hits the server.
599
601
  *
600
- * On the client, the loader's ComputedSignal `expires` is set to this value. If `poll` is true,
601
- * the signal auto-refetches when expired. If `poll` is false (default), the data is marked stale
602
- * but not auto-refetched.
603
- */
604
- readonly expires?: number;
605
- /**
606
- * When true AND `expires` is set, the loader data is automatically refetched when it expires
607
- * (polling behavior). When false (default), expired data is marked stale but not auto-refetched.
602
+ * Accepts any `cacheControl()` value (`'immutable'`, `'no-cache'`, a max-age number, an options
603
+ * object) or a function of the request event; the function may return `null` to skip the header.
604
+ * A `Cache-Control` header set inside the loader function wins over this option.
605
+ *
606
+ * Defaults to `no-cache` (always revalidate; combine with `eTag` for cheap 304s).
607
+ *
608
+ * The literal value `'immutable'` also marks the loader's data as static, so SSG writes a
609
+ * per-loader JSON file at build time.
608
610
  */
609
- readonly poll?: boolean;
611
+ readonly cacheControl?: CacheControl | ((ev: RequestEvent) => CacheControl | null);
610
612
  /**
611
613
  * Enable ETag-based caching for this loader's JSON responses.
612
614
  *
@@ -654,14 +656,6 @@ declare type LoaderOptions = {
654
656
  * triggers a re-fetch.
655
657
  */
656
658
  readonly search?: string[];
657
- /**
658
- * When true (default), the previous value is kept while the loader re-fetches after navigation,
659
- * so components see stale data until the new response arrives.
660
- *
661
- * When false, the value is cleared on re-fetch, causing reads to suspend (show a loading
662
- * boundary). This is useful when showing old data during navigation would be confusing.
663
- */
664
- readonly allowStale?: boolean;
665
659
  /**
666
660
  * When true (default), the loader is awaited before SSR renders, so its redirect or error can
667
661
  * short-circuit the response and its value is ready for synchronous reads (e.g. in the head).
@@ -1040,10 +1034,9 @@ export declare interface RouteData {
1040
1034
  * sent in the request and changes to other params are ignored. During SSR and loader JSON
1041
1035
  * requests, the loader's request event is filtered to those params too. `search: []` means no
1042
1036
  * search params are sent and only route path changes trigger a re-fetch.
1043
- * - `allowStale: false`: Clears the previous value when re-fetching, so components see a loading
1044
- * state instead of stale data during navigation. Useful when old data would be confusing.
1045
1037
  * - `eTag`: Enable ETag-based caching. Can be `true` (auto-hash), a string, or a function.
1046
- * - `expires` / `poll`: Control client-side caching and polling behavior.
1038
+ * - `cacheControl`: Cache-Control for loader JSON responses; the browser HTTP cache controls
1039
+ * client-side freshness. `'immutable'` additionally lets SSG write the loader file.
1047
1040
  *
1048
1041
  * The `strictLoaders` Vite plugin option applies `search: []` globally for all loaders that don't
1049
1042
  * specify an explicit `search` option.
@@ -3,8 +3,8 @@ import { Fragment } from '@qwik.dev/core/jsx-runtime';
3
3
  import * as qwikRouterConfig from '@qwik-router-config';
4
4
  import { p } from '@qwik.dev/core/preloader';
5
5
  import { e as ensureSlash } from './chunks/pathname.mjs';
6
- import { l as loadRoute, f as fetchRouteLoaderData, g as getClientNavPath, s as shouldPreload, i as isSamePath, t as toPath, a as isSameOrigin, Q as QACTION_KEY, e as ensureRouteLoaderSignals, b as setLoaderSignalValue, c as createDocumentHead, C as ContentContext, d as ContentInternalContext, D as DocumentHeadContext, H as HttpStatusContext, h as RouteLocationContext, j as RouteNavigateContext, k as RouteStateContext, m as RouteLoaderCtxContext, n as RouteActionContext, o as RoutePreventNavigateContext, u as updateRouteLoaderPaths, r as resolveHead, p as Q_ROUTE, q as toUrl, v as getRequestEvent, w as QFN_KEY, x as QDATA_KEY } from './chunks/head.qwik.mjs';
7
- export { y as routeLoader$, z as routeLoaderQrl } from './chunks/head.qwik.mjs';
6
+ import { l as loadRoute, g as getModuleRouteLoaders, f as fetchRouteLoaderData, a as getClientNavPath, s as shouldPreload, i as isSamePath, t as toPath, b as isSameOrigin, Q as QACTION_KEY, e as ensureRouteLoaderSignals, c as setLoaderSignalValue, d as createDocumentHead, C as ContentContext, h as ContentInternalContext, D as DocumentHeadContext, H as HttpStatusContext, j as RouteLocationContext, k as RouteNavigateContext, m as RouteStateContext, n as RouteLoaderCtxContext, o as RouteActionContext, p as RoutePreventNavigateContext, u as updateRouteLoaderPaths, q as invalidateNavRouteLoaders, r as isImmutableLoader, v as clearNavFetchCache, w as resolveHead, x as Q_ROUTE, y as toUrl, z as getRequestEvent, A as QFN_KEY, B as QDATA_KEY } from './chunks/head.qwik.mjs';
7
+ export { E as routeLoader$, G as routeLoaderQrl } from './chunks/head.qwik.mjs';
8
8
  import { u as useNavigate, a as useLocation, b as useDocumentHead, c as useQwikRouterEnv, d as useAction } from './chunks/use-functions.qwik.mjs';
9
9
  export { e as useContent, f as useHttpStatus, g as usePreventNavigate$, h as usePreventNavigateQrl } from './chunks/use-functions.qwik.mjs';
10
10
  import { _deserialize, _hasStoreEffects, _getContextContainer, forceStoreEffects, _waitUntilRendered, _getContextHostElement, _serialize } from '@qwik.dev/core/internal';
@@ -28,6 +28,30 @@ const ErrorBoundary = /* @__PURE__ */ componentQrl(/* @__PURE__ */ inlinedQrl((p
28
28
  return /* @__PURE__ */ _jsxSorted(Slot, null, null, null, 3, "bA_1");
29
29
  }, "ErrorBoundary_component_pOa6vjtC7ik"));
30
30
 
31
+ const prefetchLoaderData = (loadedRoute, url, manifestHash) => {
32
+ if (!loadedRoute.$loaders$?.length || !loadedRoute.$loaderPaths$) {
33
+ return;
34
+ }
35
+ const immutableIds = new Set(getModuleRouteLoaders(loadedRoute.$mods$).filter((loader) => loader.__cacheControl === "immutable").map((loader) => loader.__id));
36
+ const basePath = qwikRouterConfig.basePathname ?? "/";
37
+ for (const hash of loadedRoute.$loaders$) {
38
+ if (immutableIds.has(hash)) {
39
+ continue;
40
+ }
41
+ let loaderPath = loadedRoute.$loaderPaths$[hash];
42
+ if (!loaderPath) {
43
+ continue;
44
+ }
45
+ if (basePath !== "/" && !loaderPath.startsWith(basePath)) {
46
+ loaderPath = basePath + loaderPath.slice(1);
47
+ }
48
+ fetchRouteLoaderData(hash, loaderPath, manifestHash, {
49
+ pageUrl: url,
50
+ basePath
51
+ }).catch(() => {
52
+ });
53
+ }
54
+ };
31
55
  async function prefetchRoute(url, prefetchData, probability = 0.8, manifestHash, prefetchBundle = true) {
32
56
  if (!isBrowser || isDev) {
33
57
  return;
@@ -48,23 +72,7 @@ async function prefetchRoute(url, prefetchData, probability = 0.8, manifestHash,
48
72
  if (!prefetchData || !manifestHash) {
49
73
  return;
50
74
  }
51
- if (loadedRoute.$loaders$?.length && loadedRoute.$loaderPaths$) {
52
- const basePath = qwikRouterConfig.basePathname ?? "/";
53
- for (const hash of loadedRoute.$loaders$) {
54
- let loaderPath = loadedRoute.$loaderPaths$?.[hash];
55
- if (!loaderPath) {
56
- continue;
57
- }
58
- if (basePath !== "/" && !loaderPath.startsWith(basePath)) {
59
- loaderPath = basePath + loaderPath.slice(1);
60
- }
61
- fetchRouteLoaderData(hash, loaderPath, manifestHash, {
62
- pageUrl: url,
63
- basePath
64
- }).catch(() => {
65
- });
66
- }
67
- }
75
+ prefetchLoaderData(loadedRoute, url, manifestHash);
68
76
  } catch {
69
77
  }
70
78
  }
@@ -262,12 +270,10 @@ const refreshLinkPrefetchObserver = (manifestHash) => {
262
270
  cleanupPrefetchObserver?.();
263
271
  cleanupPrefetchObserver = createLinkPrefetchObserver(manifestHash);
264
272
  };
265
- const linkPrefetchInit = (manifestHash) => eventQrl(/* @__PURE__ */ inlinedQrl(() => {
266
- const manifestHash2 = _captures[0];
267
- refreshLinkPrefetchObserver(manifestHash2);
268
- }, "linkPrefetchInit_event_hoZr9O26Irg", [
269
- manifestHash
270
- ]));
273
+ const linkPrefetchInit = eventQrl(/* @__PURE__ */ inlinedQrl((_, element) => {
274
+ const manifestHash = element.closest("[q\\:manifest-hash]")?.getAttribute("q:manifest-hash") || void 0;
275
+ refreshLinkPrefetchObserver(manifestHash);
276
+ }, "linkPrefetchInit_event_hoZr9O26Irg"));
271
277
 
272
278
  function callRestoreScrollOnDocument() {
273
279
  if (document.__q_scroll_restore__) {
@@ -465,9 +471,12 @@ const spaInit = eventQrl(/* @__PURE__ */ inlinedQrl((_, el) => {
465
471
  }
466
472
  }, "spa_init_event_igI1pUsax0E"));
467
473
 
468
- async function submitAction(action, routePath) {
469
- const pathBase = ensureSlash(routePath);
470
- const url = `${pathBase}?${QACTION_KEY}=${encodeURIComponent(action.id)}`;
474
+ async function submitAction(action, routeUrl) {
475
+ const pathBase = ensureSlash(routeUrl.pathname);
476
+ const searchParams = new URLSearchParams(routeUrl.search);
477
+ searchParams.set(QACTION_KEY, action.id);
478
+ const search = searchParams.toString();
479
+ const url = `${pathBase}?${search}`;
471
480
  const actionData = action.data;
472
481
  action.data = void 0;
473
482
  let fetchOptions;
@@ -993,7 +1002,7 @@ const useQwikRouter = (props) => {
993
1002
  return;
994
1003
  }
995
1004
  if (action) {
996
- const result = await submitAction(action, trackUrl.pathname);
1005
+ const result = await submitAction(action, trackUrl);
997
1006
  if (!result) {
998
1007
  routeInternal2.untrackedValue = {
999
1008
  type: navType,
@@ -1030,6 +1039,9 @@ const useQwikRouter = (props) => {
1030
1039
  }
1031
1040
  updateRouteLoaderPaths(routeLoaderCtx2, loadedRoute.$loaderPaths$, trackUrl);
1032
1041
  const routeLoaders = ensureRouteLoaderSignals(contentModules, loaderState2, routeLoaderCtx2);
1042
+ if (!isServer) {
1043
+ invalidateNavRouteLoaders(loaderState2);
1044
+ }
1033
1045
  if (shouldInvalidateActionLoaders) {
1034
1046
  if (actionLoaderHashes !== void 0) {
1035
1047
  for (const hash of actionLoaderHashes) {
@@ -1044,9 +1056,14 @@ const useQwikRouter = (props) => {
1044
1056
  if (routeLoaders.length > 0) {
1045
1057
  for (let i = 0; i < routeLoaders.length; i++) {
1046
1058
  const loader = routeLoaders[i];
1047
- loaderState2[loader.__id].untrackedPending;
1059
+ if (!isImmutableLoader(loader.__id)) {
1060
+ loaderState2[loader.__id].untrackedPending;
1061
+ }
1048
1062
  }
1049
1063
  }
1064
+ if (!isServer) {
1065
+ clearNavFetchCache();
1066
+ }
1050
1067
  if (internalState.navCount !== navCountBefore) {
1051
1068
  return;
1052
1069
  }
@@ -1304,7 +1321,6 @@ const RouterOutlet = /* @__PURE__ */ componentQrl(/* @__PURE__ */ inlinedQrl(()
1304
1321
  throw new Error("PrefetchServiceWorker component must be rendered on the server.");
1305
1322
  }
1306
1323
  const internalContext = useContext(ContentInternalContext);
1307
- const head = useDocumentHead();
1308
1324
  const nav = useNavigate();
1309
1325
  const contents = internalContext.value;
1310
1326
  if (contents && contents.length > 0) {
@@ -1318,10 +1334,6 @@ const RouterOutlet = /* @__PURE__ */ componentQrl(/* @__PURE__ */ inlinedQrl(()
1318
1334
  return /* @__PURE__ */ _jsxSorted(Fragment, null, null, [
1319
1335
  cmp,
1320
1336
  !__EXPERIMENTAL__.noSPA && /* @__PURE__ */ _jsxSorted("script", {
1321
- "q-d:qcinit": [
1322
- spaInit,
1323
- linkPrefetchInit(head.manifestHash)
1324
- ],
1325
1337
  "q-d:qinit": _qrlSync(() => {
1326
1338
  ((w, h) => {
1327
1339
  if (!w._qcs) {
@@ -1335,9 +1347,14 @@ const RouterOutlet = /* @__PURE__ */ componentQrl(/* @__PURE__ */ inlinedQrl(()
1335
1347
  }
1336
1348
  })(window, history);
1337
1349
  }, '()=>{((w,h)=>{if(!w._qcs){w._qcs=!0;const s=h.state?._qRouterScroll;if(s){h.scrollRestoration="manual";w.scrollTo(s.x,s.y);}document.dispatchEvent(new Event("qcinit"));}})(window,history);}'),
1338
- "q-d:qrouterpopstate": /* @__PURE__ */ inlinedQrl((event, _1, nav2) => handleRouterPopstate(nav2, event), "RouterOutlet_component_Fragment_script_q_d_qrouterpopstate_aRL6GvFyews"),
1350
+ "q-d:qrouterpopstate": /* @__PURE__ */ inlinedQrl((event, _1, nav2) => handleRouterPopstate(nav2, event), "RouterOutlet_component_Fragment_script_q_d_qrouterpopstate_aRL6GvFyews").m(),
1339
1351
  "q:p": nav
1340
- }, null, null, 6, "Fn_1")
1352
+ }, {
1353
+ "q-d:qcinit": [
1354
+ spaInit,
1355
+ linkPrefetchInit
1356
+ ]
1357
+ }, null, 6, "Fn_1")
1341
1358
  ], 1, "Fn_2");
1342
1359
  }
1343
1360
  return SkipRender;
@@ -17,7 +17,7 @@ async function getConfig() {
17
17
  }
18
18
  return qwikRouterConfig;
19
19
  }
20
- function getRequestHandlerPathname(serverRequestEv) {
20
+ function getRequestHandlerPathname(serverRequestEv, isDevRequest = isDev) {
21
21
  const recognized = recognizeRequest(serverRequestEv.url.pathname);
22
22
  if (!recognized) {
23
23
  return serverRequestEv.url.pathname;
@@ -26,7 +26,8 @@ function getRequestHandlerPathname(serverRequestEv) {
26
26
  if (recognized.type !== IsQLoader) {
27
27
  return loaderPathname;
28
28
  }
29
- return resolveValidInternalFullPathname(loaderPathname, serverRequestEv.request.headers.get(FULLPATH_HEADER) ?? serverRequestEv.request.headers.get(ROUTE_PATH_HEADER)) ?? loaderPathname;
29
+ const routePath = serverRequestEv.request.headers.get(FULLPATH_HEADER) ?? (isDevRequest ? serverRequestEv.request.headers.get(ROUTE_PATH_HEADER) : null);
30
+ return resolveValidInternalFullPathname(loaderPathname, routePath) ?? loaderPathname;
30
31
  }
31
32
  async function loadRequestHandlers(qwikRouterConfig2, pathname, method, checkOrigin, renderFn, serverRequestEv) {
32
33
  const { routes, serverPlugins, cacheModules } = qwikRouterConfig2;
@@ -2041,9 +2041,9 @@ function imagePlugin(userOpts) {
2041
2041
  order: "pre",
2042
2042
  async handler(id, importer, options) {
2043
2043
  if (parseVirtualImageJsxId(id)) {
2044
- return null;
2044
+ return id;
2045
2045
  }
2046
- if (id.endsWith(TO_IMG_ID)) {
2046
+ if (id.endsWith(TO_IMG_ID) || id === VIRTUAL_TO_IMG_ID) {
2047
2047
  return VIRTUAL_TO_IMG_ID;
2048
2048
  }
2049
2049
  const { pathId, params } = parseId(id);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@qwik.dev/router",
3
3
  "description": "The router for Qwik.",
4
- "version": "2.0.0-beta.38",
4
+ "version": "2.0.0-beta.39",
5
5
  "bugs": "https://github.com/QwikDev/qwik/issues",
6
6
  "dependencies": {
7
7
  "@azure/functions": "3.5.1",
@@ -22,7 +22,7 @@
22
22
  "remark-gfm": "^4.0.1",
23
23
  "set-cookie-parser": "^2.7.2",
24
24
  "source-map": "^0.7.6",
25
- "svgo": "^3.3.3",
25
+ "svgo": "^3.3.4",
26
26
  "unified": "^11.0.5",
27
27
  "unist-util-visit": "^5.0.0",
28
28
  "valibot": ">=0.36.0 <2",
@@ -41,7 +41,7 @@
41
41
  "tsm": "2.3.0",
42
42
  "typescript": "5.9.3",
43
43
  "uvu": "0.5.6",
44
- "@qwik.dev/core": "2.0.0-beta.38"
44
+ "@qwik.dev/core": "2.0.0-beta.39"
45
45
  },
46
46
  "engines": {
47
47
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -170,7 +170,7 @@
170
170
  "main": "./lib/index.qwik.mjs",
171
171
  "peerDependencies": {
172
172
  "vite": ">=6 <9",
173
- "@qwik.dev/core": "^2.0.0-beta.38"
173
+ "@qwik.dev/core": "^2.0.0-beta.39"
174
174
  },
175
175
  "publishConfig": {
176
176
  "access": "public"