@qwik.dev/router 2.0.0-beta.41 → 2.0.0-beta.43

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.
@@ -39,6 +39,7 @@
39
39
  tiff: "image/tiff",
40
40
  ts: "video/mp2t",
41
41
  txt: "text/plain",
42
+ wasm: "application/wasm",
42
43
  wbmp: "image/vnd.wap.wbmp",
43
44
  webm: "video/webm",
44
45
  webp: "image/webp",
@@ -225,6 +225,7 @@ var RequestEvSharedNonce = "@nonce";
225
225
  var RequestEvIsRewrite = "@rewrite";
226
226
  var RequestEvShareServerTiming = "@serverTiming";
227
227
  var RequestEvETagCacheKey = "@eTagCacheKey";
228
+ var RequestEvErrorBoundaryCaught = "@errorBoundaryCaught";
228
229
  var RequestEvHttpStatusMessage = "@httpStatusMessage";
229
230
  function getRequestRoute(requestEv) {
230
231
  return requestEv[RequestEvRoute];
@@ -851,7 +852,27 @@ async function runValidators(requestEv, validators, data, devMode) {
851
852
  requestEv.send(requestEv.status(), serialized);
852
853
  };
853
854
  }
854
- var staticPaths = /* @__PURE__ */ new Set(["__QWIK_ROUTER_STATIC_PATHS_ARRAY__"]);
855
+ /**
856
+ * A set of static paths generated by the SSG, including loader json. Routes always have a trailing
857
+ * slash, short for `.../index.html`
858
+ */ var staticPaths = /* @__PURE__ */ new Set(["__QWIK_ROUTER_STATIC_PATHS_ARRAY__"]);
859
+ /**
860
+ * Check if the current pathname needs a redirect due to trailing slash mismatch. If we're not sure,
861
+ * we fall through.
862
+ *
863
+ * Note that we ignore build/ and assets/, those never need trailing slash redirects.
864
+ *
865
+ * @returns The canonical path with the correct trailing slash if a redirect is needed, otherwise
866
+ * `undefined`.
867
+ */ function getStaticPathRedirect(method, url) {
868
+ const { pathname, search } = url;
869
+ if (method.toUpperCase() !== "GET" || pathname === "/" || !pathname.startsWith("/") || pathname.startsWith("//")) return;
870
+ const hasTrailingSlash = pathname.endsWith("/");
871
+ const routesHaveTrailingSlash = !globalThis.__NO_TRAILING_SLASH__;
872
+ if (hasTrailingSlash === routesHaveTrailingSlash) return;
873
+ const canonicalPathname = hasTrailingSlash ? pathname.slice(0, -1) : pathname + "/";
874
+ if (staticPaths.has(canonicalPathname) || !routesHaveTrailingSlash && staticPaths.has(pathname)) return canonicalPathname + search;
875
+ }
855
876
  /**
856
877
  * Decide whether a given request path is a static path, for optimizing file access.
857
878
  *
@@ -862,6 +883,7 @@ var staticPaths = /* @__PURE__ */ new Set(["__QWIK_ROUTER_STATIC_PATHS_ARRAY__"]
862
883
  const p = url.pathname;
863
884
  if (p.startsWith("/" + (globalThis.__QWIK_BUILD_DIR__ || "build") + "/")) return true;
864
885
  if (p.startsWith("/" + (globalThis.__QWIK_ASSETS_DIR__ || "assets") + "/")) return true;
886
+ if (globalThis.__NO_TRAILING_SLASH__ && p !== "/") return !p.endsWith("/") && (staticPaths.has(p) || staticPaths.has(p + "/"));
865
887
  return staticPaths.has(p);
866
888
  }
867
889
  /**
@@ -1380,6 +1402,7 @@ function createResolveRequestHandlers() {
1380
1402
  if (accept && !accept.includes("text/html")) throw e;
1381
1403
  const status = e.status;
1382
1404
  requestEv.status(status);
1405
+ requestEv.headers.set("Cache-Control", "no-store");
1383
1406
  const errorLoader = route.$errorLoader$;
1384
1407
  route.$mods$ = errorLoader ? await Promise.all(errorLoader.map((load) => load())) : [await loadHttpError()];
1385
1408
  requestEv.sharedMap.set(RequestEvHttpStatusMessage, typeof e.data === "string" ? e.data : "Server Error");
@@ -1393,6 +1416,7 @@ function createResolveRequestHandlers() {
1393
1416
  requestEv.sharedMap.delete(RequestEvSharedActionId);
1394
1417
  requestEv.sharedMap.delete(RequestEvSharedActionFormData);
1395
1418
  requestEv.sharedMap.delete("@actionResult");
1419
+ requestEv.sharedMap.delete(RequestEvETagCacheKey);
1396
1420
  }
1397
1421
  async function runValidators(requestEv, validators, data) {
1398
1422
  let lastResult = {
@@ -1517,7 +1541,11 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}"
1517
1541
  if (!responseHeaders.has("Content-Type")) responseHeaders.set("Content-Type", "text/html; charset=utf-8");
1518
1542
  const cachePlan = requestEv.sharedMap.get(RequestEvETagCacheKey);
1519
1543
  const { readable, writable } = new TextEncoderStream();
1520
- const writableStream = requestEv.getWritableStream();
1544
+ let responseWriter;
1545
+ const lazyResponseSink = new WritableStream({ write(chunk) {
1546
+ responseWriter ||= requestEv.getWritableStream().getWriter();
1547
+ return responseWriter.write(chunk);
1548
+ } });
1521
1549
  let cacheChunks;
1522
1550
  let pipeSource = readable;
1523
1551
  if (cachePlan) {
@@ -1529,7 +1557,10 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}"
1529
1557
  pipeSource = readable.pipeThrough(capture);
1530
1558
  }
1531
1559
  let pipeError;
1532
- const pipe = pipeSource.pipeTo(writableStream, { preventClose: true }).catch((error) => {
1560
+ let boundaryErrored = false;
1561
+ let noStoreSent = false;
1562
+ let overrodeCacheControl = false;
1563
+ const pipe = pipeSource.pipeTo(lazyResponseSink, { preventClose: true }).catch((error) => {
1533
1564
  pipeError = error;
1534
1565
  });
1535
1566
  const stream = writable.getWriter();
@@ -1544,16 +1575,31 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}"
1544
1575
  containerAttributes: {
1545
1576
  ["q:render"]: isStatic ? "static" : "",
1546
1577
  ...serverData.containerAttributes
1578
+ },
1579
+ onBeforeFirstFlush: (info) => {
1580
+ if (info.errorBoundaryCaught) {
1581
+ boundaryErrored = true;
1582
+ noStoreSent = true;
1583
+ overrodeCacheControl = responseHeaders.has("Cache-Control");
1584
+ responseHeaders.set("Cache-Control", "no-store");
1585
+ }
1547
1586
  }
1548
1587
  });
1549
1588
  if (typeof result.html === "string") await stream.write(result.html);
1589
+ boundaryErrored ||= result.errorBoundaryCaught === true;
1590
+ if (boundaryErrored) requestEv.sharedMap.set(RequestEvErrorBoundaryCaught, true);
1550
1591
  } finally {
1551
- await stream.ready;
1552
- await stream.close();
1553
- await pipe;
1592
+ try {
1593
+ await stream.ready;
1594
+ await stream.close();
1595
+ } finally {
1596
+ await pipe;
1597
+ responseWriter?.releaseLock();
1598
+ }
1554
1599
  }
1555
1600
  if (pipeError) throw pipeError;
1556
- if (cachePlan && cacheChunks && cacheChunks.length > 0) {
1601
+ if (boundaryErrored && (noStoreSent ? cachePlan || overrodeCacheControl : !!cachePlan)) console.warn(`An <ErrorBoundary> caught during SSR of ${requestEv.url.pathname} — configured caching disabled: ` + (noStoreSent ? "the response was sent with Cache-Control: no-store." : "the SSR cache was skipped (response headers were already sent)."));
1602
+ if (cachePlan && cacheChunks && cacheChunks.length > 0 && !boundaryErrored) {
1557
1603
  const totalLength = cacheChunks.reduce((sum, chunk) => sum + chunk.length, 0);
1558
1604
  const combined = new Uint8Array(totalLength);
1559
1605
  let offset = 0;
@@ -1568,7 +1614,7 @@ The request origin "${inputOrigin}" does not match the server origin "${origin}"
1568
1614
  body: html
1569
1615
  });
1570
1616
  }
1571
- await writableStream.close();
1617
+ if (responseWriter) await requestEv.getWritableStream().close();
1572
1618
  };
1573
1619
  }
1574
1620
  function now() {
@@ -1727,4 +1773,4 @@ function runQwikRouter(serverRequestEv, loadedRoute, requestHandlers, rebuildRou
1727
1773
  };
1728
1774
  }
1729
1775
  }
1730
- export { isStaticPath as a, RewriteMessage as c, resolveRequestHandlers as i, mergeHeadersCookies as l, getErrorHtml as n, clearLoaderCache as o, renderQwikMiddleware as r, clearSsrCache as s, runQwikRouter as t };
1776
+ export { getStaticPathRedirect as a, clearSsrCache as c, mergeHeadersCookies as d, resolveRequestHandlers as i, RequestEvErrorBoundaryCaught as l, getErrorHtml as n, isStaticPath as o, renderQwikMiddleware as r, clearLoaderCache as s, runQwikRouter as t, RewriteMessage as u };
@@ -155,8 +155,16 @@ async function workerRender(sys, opts, staticRoute, pendingPromises, callback, d
155
155
  }
156
156
  }, opts, deps).then((rsp) => {
157
157
  if (rsp != null) return rsp.completion.then((r) => {
158
- if (routeWriter) return closePromise.then(() => r);
159
- return r;
158
+ const failRouteOnCaughtBoundary = (completion) => {
159
+ if (completion === void 0 && rsp.requestEv.sharedMap.get("@errorBoundaryCaught")) {
160
+ result.ok = false;
161
+ result.filePath = null;
162
+ return /* @__PURE__ */ new Error(`An <ErrorBoundary> caught an error while rendering ${staticRoute.pathname}`);
163
+ }
164
+ return completion;
165
+ };
166
+ if (routeWriter) return closePromise.then(() => failRouteOnCaughtBoundary(r));
167
+ return failRouteOnCaughtBoundary(r);
160
168
  });
161
169
  }).then((e) => {
162
170
  if (e !== void 0) {
package/lib/index.d.ts CHANGED
@@ -391,14 +391,6 @@ declare interface EndpointResponse {
391
391
  loaderHashes?: string[];
392
392
  }
393
393
 
394
- /** @public */
395
- export declare const ErrorBoundary: Component<ErrorBoundaryProps>;
396
-
397
- /** @public */
398
- declare interface ErrorBoundaryProps {
399
- fallback$?: QRL<(error: any) => any>;
400
- }
401
-
402
394
  /**
403
395
  * Drops control-flow signals (`ev.redirect()`, `ev.error()`, etc.) from a loader/action return
404
396
  * type: those are thrown, not surfaced as data. `ev.fail()` is plain data and is kept.
@@ -1,24 +1,15 @@
1
1
  import { t as ensureSlash } from "./chunks/pathname.mjs";
2
2
  import { A as routeLoader$, B as RouteLoaderCtxContext, E as isImmutableLoader, F as ContentContext, G as QACTION_KEY, H as RouteNavigateContext, I as ContentInternalContext, J as Q_ROUTE, K as QDATA_KEY, L as DocumentHeadContext, M as setLoaderSignalValue, P as updateRouteLoaderPaths, R as HttpStatusContext, T as invalidateNavRouteLoaders, U as RoutePreventNavigateContext, V as RouteLocationContext, W as RouteStateContext, _ as fetchRouteLoaderData, a as isPromise, b as getRequestEvent, c as shouldPreload, d as loadRoute, g as ensureRouteLoaderSignals, i as getClientNavPath, j as routeLoaderQrl, l as toPath, m as clearNavFetchCache, n as resolveHead, o as isSameOrigin, q as QFN_KEY, s as isSamePath, t as createDocumentHead, u as toUrl, y as getModuleRouteLoaders, z as RouteActionContext } from "./chunks/head.qwik.mjs";
3
- import { SkipRender, Slot, _captures, _fnSignal, _getConstProps, _getVarProps, _jsxSorted, _jsxSplit, _qrlSync, _restProps, _wrapProp, componentQrl, createComputedQrl, createElement, eventQrl, getLocale, implicit$FirstArg, inlinedQrl, isBrowser, isDev, isServer, noSerialize, untrack, useContext, useContextProvider, useErrorBoundary, useOnWindow, useServerData, useSignal, useStore, useTaskQrl, useVisibleTaskQrl, withLocale } from "@qwik.dev/core";
4
- import { Fragment } from "@qwik.dev/core/jsx-runtime";
3
+ import { SkipRender, Slot, _captures, _fnSignal, _getConstProps, _getVarProps, _jsxSorted, _jsxSplit, _qrlSync, _restProps, _wrapProp, componentQrl, createComputedQrl, createElement, eventQrl, getLocale, implicit$FirstArg, inlinedQrl, isBrowser, isDev, isServer, noSerialize, untrack, useContext, useContextProvider, useServerData, useSignal, useStore, useTaskQrl, useVisibleTaskQrl, withLocale } from "@qwik.dev/core";
5
4
  import * as qwikRouterConfig from "@qwik-router-config";
6
5
  import { p } from "@qwik.dev/core/preloader";
7
6
  import { _deserialize, _getContextContainer, _getContextHostElement, _hasStoreEffects, _serialize, _waitUntilRendered, forceStoreEffects } from "@qwik.dev/core/internal";
7
+ import { Fragment } from "@qwik.dev/core/jsx-runtime";
8
8
  import * as v from "valibot";
9
9
  import * as z$1 from "zod";
10
10
  import { z } from "zod";
11
11
  import swRegister from "@qwik-router-sw-register";
12
12
  import { renderToStream } from "@qwik.dev/core/server";
13
- /** @public */ var ErrorBoundary = /*#__PURE__*/ componentQrl(/*#__PURE__*/ inlinedQrl((props) => {
14
- const store = useErrorBoundary();
15
- useOnWindow("qerror", /*#__PURE__*/ inlinedQrl((e) => {
16
- const store = _captures[0];
17
- store.error = e.detail.error;
18
- }, "ErrorBoundary_component_useOnWindow_G0jFRpoNY0M", [store]));
19
- if (store.error && props.fallback$) return /*#__PURE__*/ _jsxSorted(Fragment, null, null, props.fallback$(store.error), 1, "bA_0");
20
- return /*#__PURE__*/ _jsxSorted(Slot, null, null, null, 3, "bA_1");
21
- }, "ErrorBoundary_component_pOa6vjtC7ik"));
22
13
  /**
23
14
  * Prefetch a route's loader data in parallel (fire-and-forget). The per-nav fetch map dedupes
24
15
  * repeat hovers and shares the promise with the navigation's own fetch; the browser HTTP cache
@@ -838,7 +829,7 @@ function initializeSPA(goto, scroller) {
838
829
  scroll,
839
830
  historyUpdated
840
831
  };
841
- if (isBrowser) prefetchRoute(dest, true, .8, manifestHash);
832
+ if (isBrowser) prefetchRoute(dest, false, .8, manifestHash, true);
842
833
  navResolver.p = new Promise((resolve) => {
843
834
  navResolver.r = () => {
844
835
  navResolver.r = void 0;
@@ -933,8 +924,8 @@ function initializeSPA(goto, scroller) {
933
924
  const contentModules = $mods$;
934
925
  if (!isServer) routeLoaderCtx.goto = noSerialize(goto);
935
926
  updateRouteLoaderPaths(routeLoaderCtx, loadedRoute.$loaderPaths$, trackUrl);
936
- const routeLoaders = ensureRouteLoaderSignals(contentModules, loaderState, routeLoaderCtx);
937
927
  if (!isServer) invalidateNavRouteLoaders(loaderState);
928
+ const routeLoaders = ensureRouteLoaderSignals(contentModules, loaderState, routeLoaderCtx);
938
929
  if (shouldInvalidateActionLoaders) {
939
930
  if (actionLoaderHashes !== void 0) for (const hash of actionLoaderHashes) loaderState[hash]?.invalidate(true);
940
931
  else for (const loader of routeLoaders) loaderState[loader.__id].invalidate(true);
@@ -1692,4 +1683,4 @@ var GetForm = /*#__PURE__*/ componentQrl(/*#__PURE__*/ inlinedQrl((_rawProps) =>
1692
1683
  })
1693
1684
  ], 1, "r5_3");
1694
1685
  }, "DocumentHeadTags_component_9CrWYOoCpgY"));
1695
- export { DocumentHeadTags, ErrorBoundary, Form, Link, QWIK_CITY_SCROLLER, QWIK_ROUTER_SCROLLER, QwikCityMockProvider, QwikCityProvider, QwikRouterMockProvider, QwikRouterProvider, RouterOutlet, ServiceWorkerRegister, createRenderer, getRequestEvent, globalAction$, globalActionQrl, omitProps, routeAction$, routeActionQrl, routeLoader$, routeLoaderQrl, server$, serverQrl, untypedAppUrl, useContent, useDocumentHead, useHttpStatus, useLocation, useNavigate, usePreventNavigate$, usePreventNavigateQrl, useQwikRouter, valibot$, valibotQrl, validator$, validatorQrl, z, zod$, zodQrl };
1686
+ export { DocumentHeadTags, Form, Link, QWIK_CITY_SCROLLER, QWIK_ROUTER_SCROLLER, QwikCityMockProvider, QwikCityProvider, QwikRouterMockProvider, QwikRouterProvider, RouterOutlet, ServiceWorkerRegister, createRenderer, getRequestEvent, globalAction$, globalActionQrl, omitProps, routeAction$, routeActionQrl, routeLoader$, routeLoaderQrl, server$, serverQrl, untypedAppUrl, useContent, useDocumentHead, useHttpStatus, useLocation, useNavigate, usePreventNavigate$, usePreventNavigateQrl, useQwikRouter, valibot$, valibotQrl, validator$, validatorQrl, z, zod$, zodQrl };
@@ -2,7 +2,7 @@ import { Y as _asyncRequestStore, d as loadRoute } from "../../chunks/head.qwik.
2
2
  import { c as resolveValidInternalFullPathname, l as trimInternalPathname, s as recognizeRequest, u as trimRecognizedInternalPathname } from "../../chunks/request-path.mjs";
3
3
  import { i as RedirectMessage, r as AbortMessage, t as ServerError } from "../../chunks/server-error.mjs";
4
4
  import { t as devPreloadedRouteLoaders } from "../../chunks/dev-preloaded-route-loader.mjs";
5
- import { a as isStaticPath, c as RewriteMessage, i as resolveRequestHandlers, l as mergeHeadersCookies, n as getErrorHtml, o as clearLoaderCache, r as renderQwikMiddleware, s as clearSsrCache, t as runQwikRouter } from "../../chunks/user-response.mjs";
5
+ import { a as getStaticPathRedirect, c as clearSsrCache, d as mergeHeadersCookies, i as resolveRequestHandlers, n as getErrorHtml, o as isStaticPath, r as renderQwikMiddleware, s as clearLoaderCache, t as runQwikRouter, u as RewriteMessage } from "../../chunks/user-response.mjs";
6
6
  import { isDev } from "@qwik.dev/core";
7
7
  var qwikRouterConfig;
8
8
  async function getConfig() {
@@ -38,12 +38,20 @@ async function loadRequestHandlers(qwikRouterConfig, pathname, method, checkOrig
38
38
  const config = await getConfig();
39
39
  const pathname = getRequestHandlerPathname(serverRequestEv);
40
40
  if (pathname === "/.well-known" || pathname.startsWith("/.well-known/")) return null;
41
- const { loadedRoute, requestHandlers } = await loadRequestHandlers(config, pathname, serverRequestEv.request.method, checkOrigin ?? true, render, serverRequestEv);
42
- if (config.fallthrough && loadedRoute.$notFound$) return null;
41
+ const staticPathRedirect = getStaticPathRedirect(serverRequestEv.request.method, serverRequestEv.url);
43
42
  const rebuildRouteInfo = async (url) => {
44
43
  const cleanPathname = trimInternalPathname(url.pathname);
45
44
  return loadRequestHandlers(config, cleanPathname, serverRequestEv.request.method, checkOrigin ?? true, render, serverRequestEv);
46
45
  };
46
+ if (staticPathRedirect) return runQwikRouter(serverRequestEv, {
47
+ $routeName$: pathname,
48
+ $params$: {},
49
+ $mods$: []
50
+ }, [(event) => {
51
+ throw event.redirect(301, staticPathRedirect);
52
+ }], rebuildRouteInfo, config.basePathname);
53
+ const { loadedRoute, requestHandlers } = await loadRequestHandlers(config, pathname, serverRequestEv.request.method, checkOrigin ?? true, render, serverRequestEv);
54
+ if (config.fallthrough && loadedRoute.$notFound$) return null;
47
55
  return runQwikRouter(serverRequestEv, loadedRoute, requestHandlers, rebuildRouteInfo, config.basePathname);
48
56
  }
49
57
  /**
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.41",
4
+ "version": "2.0.0-beta.43",
5
5
  "bugs": "https://github.com/QwikDev/qwik/issues",
6
6
  "dependencies": {
7
7
  "@azure/functions": "3.5.1",
@@ -41,7 +41,7 @@
41
41
  "typescript": "5.9.3",
42
42
  "uvu": "0.5.6",
43
43
  "vite": "8.2.1",
44
- "@qwik.dev/core": "2.0.0-beta.41"
44
+ "@qwik.dev/core": "2.0.0-beta.43"
45
45
  },
46
46
  "engines": {
47
47
  "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
@@ -166,7 +166,7 @@
166
166
  "main": "./lib/index.qwik.mjs",
167
167
  "peerDependencies": {
168
168
  "vite": ">=8 <9",
169
- "@qwik.dev/core": "2.0.0-beta.41"
169
+ "@qwik.dev/core": "2.0.0-beta.43"
170
170
  },
171
171
  "publishConfig": {
172
172
  "access": "public"