@rangojs/router 0.26.1 → 0.26.3
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 +11 -7
- package/dist/types/cache/cache-error.d.ts +2 -0
- package/dist/types/decode-loader-results.d.ts +12 -1
- package/dist/types/errors.d.ts +20 -0
- package/dist/types/router/loader-resolution.d.ts +10 -4
- package/dist/types/router/types.d.ts +3 -1
- package/dist/types/rsc/types.d.ts +14 -0
- package/dist/types/server/print-server-error.d.ts +27 -0
- package/dist/types/server/request-context.d.ts +6 -1
- package/dist/types/ssr/index.d.ts +14 -0
- package/dist/types/ssr/shell-error-document.d.ts +24 -0
- package/dist/types/ssr/ssr-root.d.ts +6 -0
- package/dist/types/testing/render-route.d.ts +14 -1
- package/dist/types/types/boundaries.d.ts +4 -1
- package/dist/vite/index.js +14 -9
- package/package.json +1 -1
- package/skills/loader/SKILL.md +53 -4
- package/skills/ppr/SKILL.md +19 -10
- package/skills/route/SKILL.md +4 -1
- package/skills/router-setup/SKILL.md +29 -1
- package/skills/server-actions/SKILL.md +3 -1
- package/skills/testing/client-components.md +30 -16
- package/src/cache/cache-error.ts +11 -2
- package/src/decode-loader-results.ts +29 -1
- package/src/errors.ts +39 -0
- package/src/router/loader-resolution.ts +41 -6
- package/src/router/segment-resolution/fresh.ts +2 -0
- package/src/router/segment-resolution/helpers.ts +12 -1
- package/src/router/types.ts +2 -0
- package/src/router.ts +16 -1
- package/src/rsc/handler.ts +31 -9
- package/src/rsc/loader-fetch.ts +32 -7
- package/src/rsc/progressive-enhancement.ts +13 -4
- package/src/rsc/render-pipeline.ts +9 -1
- package/src/rsc/rsc-rendering.ts +4 -0
- package/src/rsc/server-action.ts +16 -0
- package/src/rsc/ssr-setup.ts +23 -1
- package/src/rsc/types.ts +15 -0
- package/src/server/print-server-error.ts +87 -0
- package/src/server/request-context.ts +8 -0
- package/src/ssr/index.tsx +113 -10
- package/src/ssr/shell-error-document.tsx +139 -0
- package/src/ssr/ssr-root.tsx +15 -5
- package/src/testing/render-route.tsx +52 -11
- package/src/theme/ThemeProvider.tsx +31 -13
- package/src/types/boundaries.ts +8 -2
- package/src/vite/plugins/virtual-entries.ts +10 -6
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.
|
|
1647
|
+
version: "0.26.3",
|
|
1648
1648
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
1649
1649
|
keywords: [
|
|
1650
1650
|
"react",
|
|
@@ -1966,7 +1966,7 @@ import {
|
|
|
1966
1966
|
createTemporaryReferenceSet,
|
|
1967
1967
|
} from "@rangojs/router/internal/deps/browser";
|
|
1968
1968
|
import { createElement, StrictMode } from "react";
|
|
1969
|
-
import { hydrateRoot } from "react-dom/client";
|
|
1969
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
1970
1970
|
import { rscStream } from "@rangojs/router/internal/deps/html-stream-client";
|
|
1971
1971
|
import { initBrowserApp, Rango } from "@rangojs/router/browser";
|
|
1972
1972
|
|
|
@@ -2002,11 +2002,15 @@ async function initializeApp() {
|
|
|
2002
2002
|
}
|
|
2003
2003
|
|
|
2004
2004
|
const app = createElement(Rango);
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
)
|
|
2005
|
+
const root = strictMode === false ? app : createElement(StrictMode, null, app);
|
|
2006
|
+
// The server render failed its shell and answered a recovery document
|
|
2007
|
+
// (ssr/shell-error-document.tsx): nothing to hydrate, so render it, and the
|
|
2008
|
+
// router's error boundaries run as on a client navigation.
|
|
2009
|
+
if (document.documentElement.hasAttribute("data-rango-ssr-error")) {
|
|
2010
|
+
createRoot(document).render(root);
|
|
2011
|
+
} else {
|
|
2012
|
+
hydrateRoot(document, root, formState ? { formState } : undefined);
|
|
2013
|
+
}
|
|
2010
2014
|
}
|
|
2011
2015
|
|
|
2012
2016
|
initializeApp().catch(console.error);
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
*/
|
|
24
24
|
export interface CacheErrorReporter {
|
|
25
25
|
_reportBackgroundError?: (error: unknown, category: CacheErrorCategory) => void;
|
|
26
|
+
/** The errors the request printed (router/error-handling.ts printServerError). */
|
|
27
|
+
_printedErrors?: WeakSet<object>;
|
|
26
28
|
}
|
|
27
29
|
export type CacheErrorCategory =
|
|
28
30
|
/** A read failed (transient infra: KV/Cache API error). Degrade to a miss. */
|
|
@@ -11,7 +11,10 @@ import { type ErrorInfo, type ResolvedSegment } from "./types.js";
|
|
|
11
11
|
*
|
|
12
12
|
* Only decodeLoaderEntry (the read-site path — client components, so the
|
|
13
13
|
* throw lands in the router-owned boundary; during document SSR, Fizz emits
|
|
14
|
-
* the Suspense fallback and replays the throw at hydration
|
|
14
|
+
* the Suspense fallback and replays the throw at hydration, and with no
|
|
15
|
+
* Suspense above the read the throw fails the shell, which renderHTML answers
|
|
16
|
+
* with a recovery document or a 3xx, ssr/shell-error-document.tsx) throws
|
|
17
|
+
* these.
|
|
15
18
|
* decodeLoaderResults runs during the tree build on settled results, where a
|
|
16
19
|
* throw would collapse the whole payload — it routes signals through the
|
|
17
20
|
* errorFallback slot instead (same visual: the slot replaces the children
|
|
@@ -22,6 +25,12 @@ import { type ErrorInfo, type ResolvedSegment } from "./types.js";
|
|
|
22
25
|
*/
|
|
23
26
|
export declare const LOADER_NOT_FOUND_FALLBACK: unique symbol;
|
|
24
27
|
export declare const LOADER_REDIRECT: unique symbol;
|
|
28
|
+
/** The redirect a `LoaderRedirect` thrown by throwSignal carries, if any. */
|
|
29
|
+
export declare function loaderRedirectOf(error: unknown): {
|
|
30
|
+
to: string;
|
|
31
|
+
status?: number;
|
|
32
|
+
state?: unknown;
|
|
33
|
+
} | undefined;
|
|
25
34
|
/**
|
|
26
35
|
* A failed loader entry's error, rebuilt with the ErrorInfo identity
|
|
27
36
|
* (name/stack/code/cause) instead of a stripped generic Error.
|
|
@@ -29,6 +38,8 @@ export declare const LOADER_REDIRECT: unique symbol;
|
|
|
29
38
|
export declare function loaderResultError(result: {
|
|
30
39
|
error: ErrorInfo;
|
|
31
40
|
}): Error;
|
|
41
|
+
/** Whether `error` is a rebuild loaderResultError made (see the marker). */
|
|
42
|
+
export declare function isRebuiltLoaderError(error: unknown): boolean;
|
|
32
43
|
/** A loader that threw: not ok, and not a notFound()/redirect() signal. */
|
|
33
44
|
export declare function isLoaderError(result: unknown): result is {
|
|
34
45
|
error: ErrorInfo;
|
package/dist/types/errors.d.ts
CHANGED
|
@@ -39,6 +39,26 @@ export declare class DataNotFoundError extends Error {
|
|
|
39
39
|
constructor(message?: string, options?: ErrorOptions);
|
|
40
40
|
}
|
|
41
41
|
export declare function isDataNotFoundError(error: unknown): error is DataNotFoundError;
|
|
42
|
+
/**
|
|
43
|
+
* A loader's notFound() or redirect() rethrown at its read site
|
|
44
|
+
* (decode-loader-results.ts throwSignal): control flow answered with its own
|
|
45
|
+
* status, not an error to print or report. Name-based, so the RSC environment,
|
|
46
|
+
* whose module instances differ from the SSR one's, classifies it too.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isLoaderSignalError(error: unknown): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* An error React's Flight client decoded from an error row: it always sets
|
|
51
|
+
* `digest`. The Flight render printed and reported the original.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isFlightDecodedError(error: unknown): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* A render's own cancellation, not a failure: the error React's Flight and
|
|
56
|
+
* Fizz report for each pending task when a stream is aborted with no reason
|
|
57
|
+
* (a client disconnect), or an AbortError while the request itself is
|
|
58
|
+
* aborted. A loader's own AbortController or fetch timeout on a live request
|
|
59
|
+
* is a failure.
|
|
60
|
+
*/
|
|
61
|
+
export declare function isAbortedRender(error: unknown, requestSignal?: AbortSignal): boolean;
|
|
42
62
|
/**
|
|
43
63
|
* Convenience function to throw a DataNotFoundError
|
|
44
64
|
* Shorter syntax for common not-found scenarios
|
|
@@ -34,21 +34,27 @@ export type LoaderErrorCallback = (error: unknown, context: {
|
|
|
34
34
|
* presentation there.
|
|
35
35
|
* - redirect() (thrown 3xx Response): the target is resolved through the
|
|
36
36
|
* soft-redirect same-origin rules NOW so unsafe targets never leave the
|
|
37
|
-
* server; the client navigates when the entry decodes.
|
|
38
|
-
*
|
|
39
|
-
* a
|
|
37
|
+
* server; the client navigates when the entry decodes. A read outside
|
|
38
|
+
* Suspense fails the document's server-render shell before anything was
|
|
39
|
+
* sent: that document answers a real 3xx when the redirect carries no
|
|
40
|
+
* state (ssr/shell-error-document.tsx); otherwise a loader redirect is a
|
|
41
|
+
* client-side navigate.
|
|
40
42
|
*
|
|
41
43
|
* @param onError - Optional callback invoked when loader errors occur.
|
|
42
44
|
* This has a simplified signature for internal use - the caller (typically
|
|
43
45
|
* wrapLoaderPromise in router.ts) is responsible for bridging to the full
|
|
44
46
|
* OnErrorCallback with complete request context (request, url, env, etc.).
|
|
47
|
+
* @param useDefaultErrorBoundary - An `ssr: false` loader on a document or
|
|
48
|
+
* capture render: an error with no declared boundary gets
|
|
49
|
+
* `DefaultErrorFallback` (it would otherwise reject the tree), and every
|
|
50
|
+
* error answers 500.
|
|
45
51
|
*/
|
|
46
52
|
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
53
|
findNearestNotFoundBoundary: (entry: EntryData | null) => ReactNode | NotFoundBoundaryHandler | null;
|
|
48
54
|
notFoundComponent?: ReactNode | ((props: {
|
|
49
55
|
pathname: string;
|
|
50
56
|
}) => ReactNode);
|
|
51
|
-
}): Promise<LoaderDataResult<T>>;
|
|
57
|
+
}, useDefaultErrorBoundary?: boolean): Promise<LoaderDataResult<T>>;
|
|
52
58
|
/**
|
|
53
59
|
* Set up the use() method on handler context to access loaders and handles.
|
|
54
60
|
*
|
|
@@ -25,7 +25,9 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
25
25
|
env?: TEnv;
|
|
26
26
|
isPartial?: boolean;
|
|
27
27
|
requestStartTime?: number;
|
|
28
|
-
}
|
|
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;
|
|
@@ -202,6 +202,20 @@ export interface SSRRenderOptions {
|
|
|
202
202
|
* otherwise completed document. React's default console.error is kept.
|
|
203
203
|
*/
|
|
204
204
|
onError?: (error: unknown) => void;
|
|
205
|
+
/**
|
|
206
|
+
* Called once when the render failed before any HTML was produced (a shell
|
|
207
|
+
* error), before renderHTML answers it with a recovery document or rethrows
|
|
208
|
+
* it to the host (dev). `status` and `location` are the answer's, absent on
|
|
209
|
+
* a rethrow. `originated`: the error started in this render; a copy (a
|
|
210
|
+
* loader's masked rebuild, an error Flight decoded, a payload that did not
|
|
211
|
+
* decode) was reported where it started. See ssr/shell-error-document.tsx.
|
|
212
|
+
*/
|
|
213
|
+
onShellError?: (shellError: {
|
|
214
|
+
error: unknown;
|
|
215
|
+
originated: boolean;
|
|
216
|
+
status?: number;
|
|
217
|
+
location?: string;
|
|
218
|
+
}) => void;
|
|
205
219
|
}
|
|
206
220
|
/**
|
|
207
221
|
* SSR module interface for HTML rendering
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** The request context fields the record reads. */
|
|
2
|
+
interface PrintContext {
|
|
3
|
+
/** The errors this request printed (request-context.ts). */
|
|
4
|
+
_printedErrors?: WeakSet<object>;
|
|
5
|
+
request?: Request;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Print a server-side failure where it originates:
|
|
9
|
+
* `[RSC] <label> (<name>) <pathname>:` and the error object. Once per request:
|
|
10
|
+
* another place that sees the same error object in `requestContext` prints
|
|
11
|
+
* nothing more. A caller that can run outside the request's ALS frame (a
|
|
12
|
+
* Flight onError) passes the context it captured.
|
|
13
|
+
*/
|
|
14
|
+
export declare function printServerError(error: unknown, origin: {
|
|
15
|
+
label: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
pathname?: string;
|
|
18
|
+
}, requestContext: PrintContext | undefined): void;
|
|
19
|
+
/**
|
|
20
|
+
* Record an error this module did not print as done: the SSR render printed it
|
|
21
|
+
* in its own module graph, or it is a copy (a loader's masked rebuild, an
|
|
22
|
+
* error Flight decoded) whose original printed where it started.
|
|
23
|
+
*/
|
|
24
|
+
export declare function markServerErrorPrinted(error: unknown, requestContext: PrintContext | undefined): void;
|
|
25
|
+
/** Whether this request printed `error`, or recorded it as done. */
|
|
26
|
+
export declare function wasServerErrorPrinted(error: unknown, requestContext: PrintContext | undefined): boolean;
|
|
27
|
+
export {};
|
|
@@ -755,6 +755,11 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
755
755
|
_renderBarrierGuardClosed?: boolean;
|
|
756
756
|
/** @internal Per-request error dedup set for onError reporting */
|
|
757
757
|
_reportedErrors: WeakSet<object>;
|
|
758
|
+
/**
|
|
759
|
+
* @internal The errors this request printed to the server console, so a
|
|
760
|
+
* failure prints once where it originated (server/print-server-error.ts). A derived context shares it through the prototype.
|
|
761
|
+
*/
|
|
762
|
+
_printedErrors?: WeakSet<object>;
|
|
758
763
|
/**
|
|
759
764
|
* @internal Errors this response's Flight or Fizz render reported through
|
|
760
765
|
* onError: a component that threw does not fail the stream, which completes
|
|
@@ -861,7 +866,7 @@ export interface RouteRecordWindow {
|
|
|
861
866
|
* This is the type exported to library consumers. Internal code should
|
|
862
867
|
* use the full RequestContext interface directly.
|
|
863
868
|
*/
|
|
864
|
-
export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellCaptureForVisitor" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureUnrecordedLoaderPush" | "_shellCaptureLoaderLanes" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_routeRecordWindow" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
|
|
869
|
+
export type PublicRequestContext<TEnv = DefaultEnv, TParams = Record<string, string>> = Omit<RequestContext<TEnv, TParams>, "cookie" | "cookies" | "_readCookie" | "_readCookies" | "setCookie" | "deleteCookie" | "_handleStore" | "_transitionWhenRefs" | "_pprReplayPostMatchReason" | "_cacheStore" | "_prerender" | "_onDemandProducer" | "_searchParamsFilter" | "_shellCaptureRun" | "_shellCaptureForVisitor" | "_shellImplicitCache" | "_shellLoaderSeed" | "_shellCaptureLoaderRecords" | "_shellCaptureUnrecordedLoaderPush" | "_shellCaptureLoaderLanes" | "_resolvedCacheKeys" | "_shellKey" | "_shellForcedMiss" | "_prerenderWarm" | "_runAtRequestEntry" | "_routeRecordWindow" | "_shellFragmentPayload" | "_shellCaptureGuardTripped" | "_shellCaptureGuardTrippedLoaderId" | "_explicitTaggedStores" | "_requestTags" | "_recordTagOwners" | "_recordLoaderTags" | "_cacheProfiles" | "_onResponseCallbacks" | "_pendingBackgroundTasks" | "_themeConfig" | "_readTheme" | "_documentCacheRender" | "_payloadVisitorTheme" | "_payloadDeferred" | "_requestKind" | "_payloadVisitorLocation" | "_locationState" | "_routeName" | "_prevRouteKey" | "_visitorMoved" | "_inActionRevalidation" | "_actionRan" | "_reportedErrors" | "_printedErrors" | "_renderErrors" | "_recoveredHandlerErrors" | "_renderBarrier" | "_resolveRenderBarrier" | "_renderBarrierSegmentOrder" | "_treeHasStreaming" | "_renderBarrierWaiters" | "_handlerLoaderDeps" | "_awaitBeforeFlushLoaderIds" | "_renderBarrierHandleSnapshot" | "_renderBarrierGuardClosed" | "_reportBackgroundError" | "_debugPerformance" | "_metricsStore" | "_renderForeground" | "_activeRoutine" | "_renderDiagnosticsEnabled" | "_handlerStart" | "_tracing" | "_basename" | "_routerId" | "_versions" | "_setStatus" | "_rotateStateCookie" | "_setKeepCacheDirective" | "_freshReads" | "_setFreshReadsCookie" | "_requestRoot" | "_requestStart" | "_responseSent" | "_variables" | "_classifiedRoute" | "_requestMode" | "_cacheSignal" | "_dynamic" | "res">;
|
|
865
870
|
/**
|
|
866
871
|
* Marker for a waitUntil-scheduled fn whose task promise must NOT enter
|
|
867
872
|
* _pendingBackgroundTasks. Used by the PPR shell capture for its own task:
|
|
@@ -106,6 +106,20 @@ export interface SSRRenderOptions {
|
|
|
106
106
|
* otherwise completed document. React's default console.error is kept.
|
|
107
107
|
*/
|
|
108
108
|
onError?: (error: unknown) => void;
|
|
109
|
+
/**
|
|
110
|
+
* Called once when the render failed before any HTML was produced (a shell
|
|
111
|
+
* error), before renderHTML answers it or rethrows it. `status` and
|
|
112
|
+
* `location` are the answer's; both are absent when the error is rethrown
|
|
113
|
+
* to the host (dev). `originated`: the error started in this render. When
|
|
114
|
+
* false it is a copy (a loader's masked rebuild, an error Flight decoded, a
|
|
115
|
+
* payload that did not decode) whose original was reported where it started.
|
|
116
|
+
*/
|
|
117
|
+
onShellError?: (shellError: {
|
|
118
|
+
error: unknown;
|
|
119
|
+
originated: boolean;
|
|
120
|
+
status?: number;
|
|
121
|
+
location?: string;
|
|
122
|
+
}) => void;
|
|
109
123
|
}
|
|
110
124
|
/**
|
|
111
125
|
* SSR dependencies from external packages
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ResolvedThemeConfig } from "../theme/types.js";
|
|
2
|
+
import type { SSRDependencies } from "./index.js";
|
|
3
|
+
/** What renderHTML answered a shell error with. */
|
|
4
|
+
export interface ShellErrorAnswer {
|
|
5
|
+
body: ReadableStream<Uint8Array>;
|
|
6
|
+
status: number;
|
|
7
|
+
location?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* What a recovery document needs. Absent when the payload did not decode or
|
|
11
|
+
* the tree build rejected: the browser builds the same tree from the same
|
|
12
|
+
* payload and would reject the same way.
|
|
13
|
+
*/
|
|
14
|
+
export interface ShellErrorRecovery {
|
|
15
|
+
bootstrap: {
|
|
16
|
+
bootstrapScriptContent?: string;
|
|
17
|
+
bootstrapModules?: string[];
|
|
18
|
+
};
|
|
19
|
+
themeConfig: ResolvedThemeConfig | null;
|
|
20
|
+
}
|
|
21
|
+
type ShellErrorRenderDeps = Pick<SSRDependencies, "renderToReadableStream" | "injectRSCPayload">;
|
|
22
|
+
/** Answer a shell error; see the module comment for the order. */
|
|
23
|
+
export declare function answerShellError(deps: ShellErrorRenderDeps, error: unknown, recovery: ShellErrorRecovery | undefined, flight: ReadableStream<Uint8Array> | undefined, nonce: string | undefined): Promise<ShellErrorAnswer>;
|
|
24
|
+
export {};
|
|
@@ -80,6 +80,12 @@ export interface SsrRootOptions {
|
|
|
80
80
|
* live fizz passes the request's origin; capture/resume pass theirs.
|
|
81
81
|
*/
|
|
82
82
|
origin?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Called when the segment tree build rejects. renderHTML answers that shell
|
|
85
|
+
* error with a static document: the browser builds the same tree from the
|
|
86
|
+
* same payload and would reject the same way (ssr/shell-error-document.tsx).
|
|
87
|
+
*/
|
|
88
|
+
onTreeError?: (error: unknown) => void;
|
|
83
89
|
}
|
|
84
90
|
/**
|
|
85
91
|
* Build the closure component that deserializes the Flight payload, consumes
|
|
@@ -299,7 +299,12 @@ export interface RenderRouteOptions {
|
|
|
299
299
|
* - Content that suspends in the server pass (`lazy`, a pending or untracked
|
|
300
300
|
* `use()` promise) is emitted as its fallback and client-rendered, not
|
|
301
301
|
* hydrated; React reports the boundary in `recoverableErrors`. Outside any
|
|
302
|
-
* `<Suspense>` it rejects
|
|
302
|
+
* `<Suspense>` it rejects: production's server waits for it, this
|
|
303
|
+
* synchronous pass cannot.
|
|
304
|
+
* - An error the server pass throws outside any `<Suspense>` fails the
|
|
305
|
+
* shell. Production answers a recovery document and the browser renders
|
|
306
|
+
* the tree with `createRoot` instead of hydrating it; so does this mode,
|
|
307
|
+
* and the result's `shellError` holds the error.
|
|
303
308
|
* - Needs @testing-library/react >= 16.2.0; throws on older versions.
|
|
304
309
|
*/
|
|
305
310
|
hydrate?: boolean;
|
|
@@ -389,6 +394,14 @@ export type RenderRouteHydrateResult = RenderRouteResult & {
|
|
|
389
394
|
* with `console.error` in development.
|
|
390
395
|
*/
|
|
391
396
|
recoverableErrors: string[];
|
|
397
|
+
/**
|
|
398
|
+
* The error the server pass threw outside any `<Suspense>` (a shell error),
|
|
399
|
+
* or undefined. When set, the tree was rendered with `createRoot`, as the
|
|
400
|
+
* browser renders production's recovery document, so the router's error
|
|
401
|
+
* boundaries ran on the client: assert what they show. `serverHtml` is then
|
|
402
|
+
* empty.
|
|
403
|
+
*/
|
|
404
|
+
shellError: unknown;
|
|
392
405
|
};
|
|
393
406
|
export declare function renderRoute(routes: RenderRouteSpec[], options: RenderRouteOptions & {
|
|
394
407
|
hydrate: true;
|
|
@@ -79,10 +79,13 @@ export type LoaderDataResult<T = unknown> = {
|
|
|
79
79
|
* resolved `redirect(url, { state })` record (`__rsc_ls_*` keys) — it
|
|
80
80
|
* travels on the marker because a streaming loader settles after
|
|
81
81
|
* payload metadata flushed; the redirect navigation merges it at the
|
|
82
|
-
* target entry.
|
|
82
|
+
* target entry. `status` is the redirect's own: a document whose shell
|
|
83
|
+
* the read fails answers it as a real redirect when there is no state
|
|
84
|
+
* (ssr/shell-error-document.tsx).
|
|
83
85
|
*/
|
|
84
86
|
redirect?: {
|
|
85
87
|
to: string;
|
|
88
|
+
status?: number;
|
|
86
89
|
state?: Record<string, unknown>;
|
|
87
90
|
};
|
|
88
91
|
};
|
package/dist/vite/index.js
CHANGED
|
@@ -5209,7 +5209,7 @@ import { useContext as useContext13, useEffect as useEffect11, useRef as useRef9
|
|
|
5209
5209
|
import {
|
|
5210
5210
|
useState as useState11,
|
|
5211
5211
|
useEffect as useEffect13,
|
|
5212
|
-
useLayoutEffect as
|
|
5212
|
+
useLayoutEffect as useLayoutEffect4,
|
|
5213
5213
|
useCallback as useCallback4,
|
|
5214
5214
|
useMemo as useMemo7,
|
|
5215
5215
|
useRef as useRef11,
|
|
@@ -5225,6 +5225,7 @@ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
|
5225
5225
|
import {
|
|
5226
5226
|
useCallback as useCallback3,
|
|
5227
5227
|
useEffect as useEffect12,
|
|
5228
|
+
useLayoutEffect as useLayoutEffect3,
|
|
5228
5229
|
useMemo as useMemo6,
|
|
5229
5230
|
useState as useState10
|
|
5230
5231
|
} from "react";
|
|
@@ -5877,7 +5878,7 @@ import {
|
|
|
5877
5878
|
Fragment as Fragment4,
|
|
5878
5879
|
Suspense as Suspense3,
|
|
5879
5880
|
useDeferredValue,
|
|
5880
|
-
useLayoutEffect as
|
|
5881
|
+
useLayoutEffect as useLayoutEffect5,
|
|
5881
5882
|
useMemo as useMemo10,
|
|
5882
5883
|
useState as useState13
|
|
5883
5884
|
} from "react";
|
|
@@ -7077,7 +7078,7 @@ function resolveVersionsFrom(table, routerId2) {
|
|
|
7077
7078
|
// package.json
|
|
7078
7079
|
var package_default = {
|
|
7079
7080
|
name: "@rangojs/router",
|
|
7080
|
-
version: "0.26.
|
|
7081
|
+
version: "0.26.3",
|
|
7081
7082
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
7082
7083
|
keywords: [
|
|
7083
7084
|
"react",
|
|
@@ -7387,7 +7388,7 @@ import {
|
|
|
7387
7388
|
createTemporaryReferenceSet,
|
|
7388
7389
|
} from "@rangojs/router/internal/deps/browser";
|
|
7389
7390
|
import { createElement, StrictMode } from "react";
|
|
7390
|
-
import { hydrateRoot } from "react-dom/client";
|
|
7391
|
+
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
7391
7392
|
import { rscStream } from "@rangojs/router/internal/deps/html-stream-client";
|
|
7392
7393
|
import { initBrowserApp, Rango } from "@rangojs/router/browser";
|
|
7393
7394
|
|
|
@@ -7423,11 +7424,15 @@ async function initializeApp() {
|
|
|
7423
7424
|
}
|
|
7424
7425
|
|
|
7425
7426
|
const app = createElement(Rango);
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
)
|
|
7427
|
+
const root = strictMode === false ? app : createElement(StrictMode, null, app);
|
|
7428
|
+
// The server render failed its shell and answered a recovery document
|
|
7429
|
+
// (ssr/shell-error-document.tsx): nothing to hydrate, so render it, and the
|
|
7430
|
+
// router's error boundaries run as on a client navigation.
|
|
7431
|
+
if (document.documentElement.hasAttribute("data-rango-ssr-error")) {
|
|
7432
|
+
createRoot(document).render(root);
|
|
7433
|
+
} else {
|
|
7434
|
+
hydrateRoot(document, root, formState ? { formState } : undefined);
|
|
7435
|
+
}
|
|
7431
7436
|
}
|
|
7432
7437
|
|
|
7433
7438
|
initializeApp().catch(console.error);
|
package/package.json
CHANGED
package/skills/loader/SKILL.md
CHANGED
|
@@ -980,10 +980,10 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
980
980
|
|
|
981
981
|
Semantics by lane:
|
|
982
982
|
|
|
983
|
-
| Signal | Document load
|
|
984
|
-
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
985
|
-
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `ssr: false` (below) makes it deterministic. | 404 UI swaps in, URL preserved, payload stays 200 |
|
|
986
|
-
| `redirect()` | 200 document, then a client-side replace to the target
|
|
983
|
+
| Signal | Document load, read inside `<Suspense>` or under `loading()` | Document load, read outside any `<Suspense>` | Client navigation |
|
|
984
|
+
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
|
985
|
+
| `notFound()` | Not-found UI resolves server-side (nearest `notFoundBoundary` → router option → default) and rides the envelope; the 404 STATUS is **opportunistic** — real only if the rejection beats Response construction. `ssr: false` (below) makes it deterministic. | A real 404; the browser renders the not-found UI with the layouts kept (the recovery document, below) | 404 UI swaps in, URL preserved, payload stays 200 |
|
|
986
|
+
| `redirect()` | 200 document, then a client-side replace to the target | A real 3xx (the redirect's own status) with `Location`; with `state`, the recovery document (200), then the client replace carries it | Redirect envelope navigates to the target |
|
|
987
987
|
|
|
988
988
|
With `loader(Def, { ssr: false })` the result is settled BEFORE the document
|
|
989
989
|
flushes, so both signals are resolved server-side while the tree is built:
|
|
@@ -995,6 +995,55 @@ 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 in every
|
|
1002
|
+
case, as a handler that throws does. Layouts above it stay. On a `ppr` shell HIT whose run throws, the status
|
|
1003
|
+
went out with the shell (200), the baked content shows first, and the boundary
|
|
1004
|
+
replaces it at hydration, after React reports a hydration mismatch.
|
|
1005
|
+
|
|
1006
|
+
### A read outside any `<Suspense>` on a document load
|
|
1007
|
+
|
|
1008
|
+
A read that throws with no `<Suspense>` above it (no `loading()`, no inline
|
|
1009
|
+
boundary) fails React's server render before any HTML exists: React's server
|
|
1010
|
+
renderer runs no error boundaries. In production the router then answers a
|
|
1011
|
+
small recovery document that carries the same page data, and the browser
|
|
1012
|
+
renders the page instead of hydrating it, so the document ends where a client
|
|
1013
|
+
navigation to the URL ends:
|
|
1014
|
+
|
|
1015
|
+
| What the read throws | Document status | What the browser shows |
|
|
1016
|
+
| -------------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
|
|
1017
|
+
| An error | 500 | The route's `errorBoundary()` (or `defaultErrorBoundary`) with the layouts kept; with neither, `RootErrorBoundary` |
|
|
1018
|
+
| `notFound()` | 404 | The not-found UI, layouts kept |
|
|
1019
|
+
| `redirect()` | the redirect's 3xx | The target (the browser follows `Location`) |
|
|
1020
|
+
| `redirect(url, { state })` | 200 | The target, after the client replace that carries the state |
|
|
1021
|
+
|
|
1022
|
+
The same holds for any client component that throws only during the server
|
|
1023
|
+
render: the recovery document is a 500, and the page renders in the browser.
|
|
1024
|
+
In dev an error keeps Vite's error overlay with the original stack;
|
|
1025
|
+
`notFound()` and `redirect()` are answered as in production. A `ppr` route
|
|
1026
|
+
never stores such a document as its shell, and the document cache never stores
|
|
1027
|
+
it. `onError` gets the loader's own report (phase `loader`), with the original
|
|
1028
|
+
error, and no second report for the document. `notFound()` and
|
|
1029
|
+
`redirect()` are control flow: neither is reported nor printed.
|
|
1030
|
+
|
|
1031
|
+
A read under `loading()` or a `<Suspense>` streams the page with that
|
|
1032
|
+
boundary's fallback and keeps the 200 it already sent.
|
|
1033
|
+
|
|
1034
|
+
### What the server prints
|
|
1035
|
+
|
|
1036
|
+
A loader that throws prints its original error, message and stack, to the
|
|
1037
|
+
server console once per request, where it ran:
|
|
1038
|
+
`[RSC] Loader error (ProductLoader) /product/1:` and the error. That holds
|
|
1039
|
+
whatever renders it (a route `errorBoundary()`, `defaultErrorBoundary`, the
|
|
1040
|
+
default page, `RootErrorBoundary` in the browser), on every lane (a document,
|
|
1041
|
+
a client navigation, an action's revalidation, a prefetch, a `ppr` capture),
|
|
1042
|
+
in dev and production. Only the copy the browser receives is masked
|
|
1043
|
+
(`An error occurred` in production). A handler that reads the same loader
|
|
1044
|
+
with `ctx.use()` does not print it again. `notFound()` and `redirect()` never
|
|
1045
|
+
print.
|
|
1046
|
+
|
|
998
1047
|
Session/auth gates belong in middleware (they are request-shaped, not
|
|
999
1048
|
data-shaped, and middleware CAN emit a real pre-stream 302). Data-dependent
|
|
1000
1049
|
"this slug moved / does not exist" belongs in the loader.
|
package/skills/ppr/SKILL.md
CHANGED
|
@@ -733,8 +733,11 @@ boundary, and streamed fresh on every request. It needs a boundary —
|
|
|
733
733
|
`loading()` or an inline `<Suspense>` above the reader — or the shell is
|
|
734
734
|
refused: a masked read with no boundary above it root-postpones, the `<body>`
|
|
735
735
|
sanity gate refuses the capture, and the route stays an eternal MISS (warned
|
|
736
|
-
once per key).
|
|
737
|
-
|
|
736
|
+
once per key). When that read also throws on the MISS render, the document is
|
|
737
|
+
the 500 recovery document the `/loader` skill describes ("A read outside any
|
|
738
|
+
`<Suspense>`"), and nothing is stored. A route whose loaders are ALL
|
|
739
|
+
`ssr: false` needs no `loading()`: nothing masks, and the shell captures
|
|
740
|
+
complete.
|
|
738
741
|
|
|
739
742
|
```typescript
|
|
740
743
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
@@ -926,9 +929,10 @@ by a navigation alone carries no pins, and its replay runs these loaders fresh.
|
|
|
926
929
|
fill the holes — and the recorded baked paths are overlaid onto the fresh
|
|
927
930
|
result, so the payload matches the prelude while the nested promises stream
|
|
928
931
|
fresh. A fresh rejection skips the overlay and goes to the loader's error
|
|
929
|
-
boundary
|
|
930
|
-
|
|
931
|
-
|
|
932
|
+
boundary (the route's `errorBoundary()`, else the router's default one): on
|
|
933
|
+
a document HIT the payload then carries the error where the HTML has the
|
|
934
|
+
baked value, and React renders that part again on the client (one
|
|
935
|
+
hydration mismatch, the one known residual of a HIT). Give it its own cache,
|
|
932
936
|
`loader(Def, { ssr: false }, () => [cache({ ttl })])`, so that run reads
|
|
933
937
|
through the loader cache.
|
|
934
938
|
|
|
@@ -1119,7 +1123,8 @@ Four hard edges (each e2e/unit-pinned):
|
|
|
1119
1123
|
- **A rejecting bake-lane loader refuses.** Error UI never bakes. So does a
|
|
1120
1124
|
value that fails to encode: a function, a class instance, a promise inside a
|
|
1121
1125
|
`Map` that rejects, or a server component in the value that throws. The
|
|
1122
|
-
capture stores nothing,
|
|
1126
|
+
capture stores nothing, prints the error once (a loader's as
|
|
1127
|
+
`[RSC] Loader error`), reports `cache-write` and backs the
|
|
1123
1128
|
key off, the same as a shell component that throws (below). The capture
|
|
1124
1129
|
encodes the value a second time to pin it for HITs, so a server component
|
|
1125
1130
|
that throws only on that run refuses it too. A build-time capture skips the
|
|
@@ -1611,14 +1616,18 @@ evicted by tag at all — move always-fresh data into a live-lane loader (no
|
|
|
1611
1616
|
- **HIT status is committed at the flush**: a failing hole cannot become a
|
|
1612
1617
|
500/redirect after the first shell byte — error UI renders inline via
|
|
1613
1618
|
Suspense/error boundaries (the same property any streamed SSR page has after
|
|
1614
|
-
its shell flushes).
|
|
1619
|
+
its shell flushes). A MISS has sent nothing yet: an `ssr: false` loader that
|
|
1620
|
+
throws answers 500 there with its route's `errorBoundary()` (or the default
|
|
1621
|
+
one), and the capture stores nothing.
|
|
1615
1622
|
- **A shell component that throws during capture refuses it**: an async
|
|
1616
1623
|
server component (or a client component during SSR) inside a Suspense
|
|
1617
1624
|
boundary that throws does not fail the capture render, so its errored
|
|
1618
1625
|
boundary would bake into the shared prelude. The capture stores nothing
|
|
1619
|
-
instead, logs `[ShellCache] capture` with the error
|
|
1620
|
-
|
|
1621
|
-
|
|
1626
|
+
instead, logs `[ShellCache] capture` with the error unless the error was
|
|
1627
|
+
already printed where it happened (a server component's or a loader's
|
|
1628
|
+
error prints once, as `[RSC] Rendering error` or `[RSC] Loader error`), it
|
|
1629
|
+
goes to `onError` as `cache-write` unless the router already reported that
|
|
1630
|
+
same error during the render, and backs the key off like any refused capture; the next
|
|
1622
1631
|
capture after the window stores normally. The request that triggered the
|
|
1623
1632
|
capture is served as before. Only a throw during the capture counts: a
|
|
1624
1633
|
component that failed in the live render and succeeds when captured is
|
package/skills/route/SKILL.md
CHANGED
|
@@ -564,7 +564,10 @@ path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
|
564
564
|
`loading(fallback, { ssr: false })` on a document load) still sets 500 / 404.
|
|
565
565
|
A loader's 404 is likewise only set when its rejection settles before the
|
|
566
566
|
response is built.
|
|
567
|
-
- `onError` fires once per failure
|
|
567
|
+
- `onError` fires once per failure, and the server console prints the
|
|
568
|
+
original error once, where it happened
|
|
569
|
+
(`[RSC] Handler error (<segment>) <path>:`); only what the browser receives
|
|
570
|
+
is masked.
|
|
568
571
|
- A failed render is never stored: `cache()` skips the write for the scope that
|
|
569
572
|
holds the failed segment, a `ppr` shell and the document cache refuse the
|
|
570
573
|
whole document, and a Prerender route still fails the build.
|
|
@@ -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
|
|
@@ -706,6 +706,34 @@ The resolver receives `{ request, env, url }` and may be async. It runs only
|
|
|
706
706
|
for HTML document responses; RSC payloads (navigations, prefetches, `__rsc`
|
|
707
707
|
requests) and response routes are unaffected.
|
|
708
708
|
|
|
709
|
+
### When the server render fails before any HTML
|
|
710
|
+
|
|
711
|
+
An error thrown during the server render with no `<Suspense>` above it (a
|
|
712
|
+
`useLoader` read of a failed loader, a client component that throws only on
|
|
713
|
+
the server) leaves React no HTML to send: its server renderer runs no error
|
|
714
|
+
boundaries. In production the router answers a small recovery document
|
|
715
|
+
instead of the host's raw 500. It carries the page's data, and the generated
|
|
716
|
+
browser entry renders the page with `createRoot` instead of hydrating it, so
|
|
717
|
+
the route's `errorBoundary()`, `defaultErrorBoundary` or the root error page
|
|
718
|
+
shows with the layouts, as after a client navigation. The status is 500, 404
|
|
719
|
+
for a loader's `notFound()`, and a loader's `redirect()` without state answers
|
|
720
|
+
a real 3xx (with state, 200 and the client replace). When the page data itself failed, the answer is a static error
|
|
721
|
+
page with no scripts and nothing from the error. In dev the error reaches
|
|
722
|
+
Vite's error overlay unchanged. The `/loader` skill has the full table.
|
|
723
|
+
|
|
724
|
+
The browser entry is generated by `rango()`. A hand-written browser entry must
|
|
725
|
+
make the same choice, or React reports a hydration error (#418) on the
|
|
726
|
+
recovery document before it renders the same page:
|
|
727
|
+
|
|
728
|
+
```typescript
|
|
729
|
+
const root = createElement(StrictMode, null, createElement(Rango));
|
|
730
|
+
if (document.documentElement.hasAttribute("data-rango-ssr-error")) {
|
|
731
|
+
createRoot(document).render(root);
|
|
732
|
+
} else {
|
|
733
|
+
hydrateRoot(document, root, formState ? { formState } : undefined);
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
709
737
|
## Timeouts
|
|
710
738
|
|
|
711
739
|
Off by default. A timed-out action or render start returns a `504` (with an
|
|
@@ -688,7 +688,9 @@ layout(CheckoutLayout, () => [
|
|
|
688
688
|
|
|
689
689
|
In production `error.message` is replaced with `"An error occurred"` (and
|
|
690
690
|
`stack` / `cause` are dropped), while `error.name` and `error.code` are kept —
|
|
691
|
-
branch on `name` or `code`, not on the message.
|
|
691
|
+
branch on `name` or `code`, not on the message. The server console prints the
|
|
692
|
+
original once, message and stack, JS or no-JS, boundary or not:
|
|
693
|
+
`[RSC] Action error (<action id>) <path>:`.
|
|
692
694
|
|
|
693
695
|
### Not found from an action
|
|
694
696
|
|