@rangojs/router 0.26.1 → 0.26.2

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/dist/bin/rango.js CHANGED
@@ -1644,7 +1644,7 @@ var init_package = __esm({
1644
1644
  "package.json"() {
1645
1645
  package_default = {
1646
1646
  name: "@rangojs/router",
1647
- version: "0.26.1",
1647
+ version: "0.26.2",
1648
1648
  description: "Django-inspired RSC router with composable URL patterns",
1649
1649
  keywords: [
1650
1650
  "react",
@@ -42,13 +42,16 @@ export type LoaderErrorCallback = (error: unknown, context: {
42
42
  * This has a simplified signature for internal use - the caller (typically
43
43
  * wrapLoaderPromise in router.ts) is responsible for bridging to the full
44
44
  * OnErrorCallback with complete request context (request, url, env, etc.).
45
+ * @param useDefaultErrorBoundary - An `ssr: false` loader on a document or
46
+ * capture render: an error with no declared boundary gets
47
+ * `DefaultErrorFallback` and a 500 (it would otherwise reject the tree).
45
48
  */
46
49
  export declare function wrapLoaderWithErrorHandling<T>(promise: Promise<T>, entry: EntryData, segmentId: string, pathname: string, findNearestErrorBoundary: (entry: EntryData | null) => ReactNode | ErrorBoundaryHandler | null, createErrorInfo: (error: unknown, segmentId: string, segmentType: ErrorInfo["segmentType"]) => ErrorInfo, onError?: LoaderErrorCallback, notFoundDeps?: {
47
50
  findNearestNotFoundBoundary: (entry: EntryData | null) => ReactNode | NotFoundBoundaryHandler | null;
48
51
  notFoundComponent?: ReactNode | ((props: {
49
52
  pathname: string;
50
53
  }) => ReactNode);
51
- }): Promise<LoaderDataResult<T>>;
54
+ }, useDefaultErrorBoundary?: boolean): Promise<LoaderDataResult<T>>;
52
55
  /**
53
56
  * Set up the use() method on handler context to access loaders and handles.
54
57
  *
@@ -25,7 +25,9 @@ export interface SegmentResolutionDeps<TEnv = any> {
25
25
  env?: TEnv;
26
26
  isPartial?: boolean;
27
27
  requestStartTime?: number;
28
- }) => Promise<LoaderDataResult<T>>;
28
+ },
29
+ /** An `ssr: false` loader on a document or capture render. */
30
+ useDefaultErrorBoundary?: boolean) => Promise<LoaderDataResult<T>>;
29
31
  trackHandler: <T>(promise: Promise<T>, errorContext?: {
30
32
  segmentId?: string;
31
33
  segmentType?: string;
@@ -7077,7 +7077,7 @@ function resolveVersionsFrom(table, routerId2) {
7077
7077
  // package.json
7078
7078
  var package_default = {
7079
7079
  name: "@rangojs/router",
7080
- version: "0.26.1",
7080
+ version: "0.26.2",
7081
7081
  description: "Django-inspired RSC router with composable URL patterns",
7082
7082
  keywords: [
7083
7083
  "react",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.26.1",
3
+ "version": "0.26.2",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
@@ -995,6 +995,15 @@ runs, so a `useLoader` in a layout above every Suspense boundary is safe. On a
995
995
  during the shell capture refuses the capture (warned once per key): that URL
996
996
  gets no shell, and its renders deliver the signal as above.
997
997
 
998
+ A flagged loader that throws (anything but `notFound()` or `redirect()`) is
999
+ resolved the same way: its route's `errorBoundary()` renders at the owning
1000
+ segment, else `createRouter({ defaultErrorBoundary })`, else the router's
1001
+ default error boundary (the "Internal Server Error" page) with a 500, as a
1002
+ handler that throws does; a declared `errorBoundary()` or
1003
+ `defaultErrorBoundary` answers 200 here. Layouts above it stay. On a `ppr` shell HIT whose
1004
+ run throws, the baked content shows first and the boundary replaces it at
1005
+ hydration, after React reports a hydration mismatch.
1006
+
998
1007
  Session/auth gates belong in middleware (they are request-shaped, not
999
1008
  data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
1000
1009
  "this slug moved / does not exist" belongs in the loader.
@@ -926,9 +926,10 @@ by a navigation alone carries no pins, and its replay runs these loaders fresh.
926
926
  fill the holes — and the recorded baked paths are overlaid onto the fresh
927
927
  result, so the payload matches the prelude while the nested promises stream
928
928
  fresh. A fresh rejection skips the overlay and goes to the loader's error
929
- boundary: on a document HIT the payload then carries the error where the
930
- HTML has the baked value, and React renders that part again on the client
931
- (one hydration mismatch, the one known residual of a HIT). Give it its own cache,
929
+ boundary (the route's `errorBoundary()`, else the router's default one): on
930
+ a document HIT the payload then carries the error where the HTML has the
931
+ baked value, and React renders that part again on the client (one
932
+ hydration mismatch, the one known residual of a HIT). Give it its own cache,
932
933
  `loader(Def, { ssr: false }, () => [cache({ ttl })])`, so that run reads
933
934
  through the loader cache.
934
935
 
@@ -130,7 +130,7 @@ interface RangoOptions<TEnv> {
130
130
  id?: string; // Router id; namespaces build output and route maps. Auto-generated.
131
131
 
132
132
  // --- Fallbacks and errors ---
133
- defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler; // When no errorBoundary() matches
133
+ defaultErrorBoundary?: ReactNode | ErrorBoundaryHandler; // When no errorBoundary() matches; unset: the built-in "Internal Server Error" page
134
134
  defaultNotFoundBoundary?: ReactNode | NotFoundBoundaryHandler; // When no notFoundBoundary() matches
135
135
  notFound?: ReactNode | ((props: { pathname: string }) => ReactNode); // 404 page
136
136
  onError?: OnErrorCallback<TEnv>; // Logging/reporting hook; cannot change the response
@@ -55,6 +55,7 @@ import {
55
55
  } from "../server/handle-store.js";
56
56
  import { getFetchableLoader } from "../server/fetchable-loader-store.js";
57
57
  import { _getRequestContext } from "../server/request-context.js";
58
+ import { DefaultErrorFallback } from "../default-error-boundary.js";
58
59
  import { defineRewrittenFrom } from "../server/rewrite-trail.js";
59
60
  import {
60
61
  firstUnitCandidate,
@@ -154,6 +155,9 @@ function pushHandleValue(
154
155
  * This has a simplified signature for internal use - the caller (typically
155
156
  * wrapLoaderPromise in router.ts) is responsible for bridging to the full
156
157
  * OnErrorCallback with complete request context (request, url, env, etc.).
158
+ * @param useDefaultErrorBoundary - An `ssr: false` loader on a document or
159
+ * capture render: an error with no declared boundary gets
160
+ * `DefaultErrorFallback` and a 500 (it would otherwise reject the tree).
157
161
  */
158
162
  export function wrapLoaderWithErrorHandling<T>(
159
163
  promise: Promise<T>,
@@ -177,13 +181,15 @@ export function wrapLoaderWithErrorHandling<T>(
177
181
  | ReactNode
178
182
  | ((props: { pathname: string }) => ReactNode);
179
183
  },
184
+ useDefaultErrorBoundary?: boolean,
180
185
  ): Promise<LoaderDataResult<T>> {
181
186
  // Extract the trailing token from segmentId (format: "<shortCode>D<i>.<loaderId>").
182
187
  // The token is the loader's $$id (hash#export in prod, pathfrag#export in dev),
183
188
  // not a clean display name.
184
189
  const loaderName = segmentId.split(".").pop() || "unknown";
185
190
  // Captured now: the rejection can settle outside the request's ALS frame.
186
- const renderErrors = _getRequestContext()?._renderErrors;
191
+ const requestContext = _getRequestContext();
192
+ const renderErrors = requestContext?._renderErrors;
187
193
 
188
194
  return Promise.resolve(promise)
189
195
  .then(
@@ -286,7 +292,9 @@ export function wrapLoaderWithErrorHandling<T>(
286
292
  renderErrors?.push(error);
287
293
 
288
294
  // Find nearest error boundary
289
- const fallback = findNearestErrorBoundary(entry);
295
+ const declared = findNearestErrorBoundary(entry);
296
+ const fallback =
297
+ declared ?? (useDefaultErrorBoundary ? DefaultErrorFallback : null);
290
298
 
291
299
  // Create error info
292
300
  const errorInfo = createErrorInfo(error, segmentId, "loader");
@@ -341,6 +349,8 @@ export function wrapLoaderWithErrorHandling<T>(
341
349
  segmentId,
342
350
  message: errorInfo.message,
343
351
  });
352
+ // The default boundary answers 500, as in catchSegmentError.
353
+ if (!declared) requestContext?._setStatus(500);
344
354
 
345
355
  return {
346
356
  __loaderResult: true,
@@ -184,6 +184,7 @@ export async function resolveLoaders<TEnv>(
184
184
  segmentId,
185
185
  ctx.pathname,
186
186
  errorContext,
187
+ loaderEntry.awaitBeforeFlush,
187
188
  ),
188
189
  belongsToRoute,
189
190
  };
@@ -235,6 +236,7 @@ export async function resolveLoaders<TEnv>(
235
236
  segmentId,
236
237
  ctx.pathname,
237
238
  errorContext,
239
+ loaderEntry.awaitBeforeFlush,
238
240
  );
239
241
  return { wrapped, segmentId };
240
242
  });
@@ -49,6 +49,8 @@ export interface SegmentResolutionDeps<TEnv = any> {
49
49
  isPartial?: boolean;
50
50
  requestStartTime?: number;
51
51
  },
52
+ /** An `ssr: false` loader on a document or capture render. */
53
+ useDefaultErrorBoundary?: boolean,
52
54
  ) => Promise<LoaderDataResult<T>>;
53
55
  trackHandler: <T>(
54
56
  promise: Promise<T>,
package/src/router.ts CHANGED
@@ -531,6 +531,7 @@ export function createRouter<TEnv = any>(
531
531
  isPartial?: boolean;
532
532
  requestStartTime?: number;
533
533
  },
534
+ useDefaultErrorBoundary?: boolean,
534
535
  ): Promise<LoaderDataResult<T>> {
535
536
  const loaderStart = telemetrySink ? performance.now() : 0;
536
537
  const loaderRequestId = telemetrySink
@@ -600,6 +601,7 @@ export function createRouter<TEnv = any>(
600
601
  findNearestNotFoundBoundary,
601
602
  notFoundComponent: notFound,
602
603
  },
604
+ useDefaultErrorBoundary,
603
605
  );
604
606
 
605
607
  // Emit loader.end after the promise settles (fire-and-forget)