@rangojs/router 0.0.0-experimental.132 → 0.0.0-experimental.133
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/AGENTS.md +8 -0
- package/README.md +43 -2
- package/dist/bin/rango.js +92 -16
- package/dist/vite/index.js +166 -70
- package/package.json +19 -18
- package/skills/breadcrumbs/SKILL.md +1 -1
- package/skills/bundle-analysis/SKILL.md +2 -2
- package/skills/cache-guide/SKILL.md +2 -2
- package/skills/caching/SKILL.md +16 -9
- package/skills/debug-manifest/SKILL.md +4 -2
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/handler-use/SKILL.md +1 -1
- package/skills/hooks/SKILL.md +2 -2
- package/skills/host-router/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +1 -1
- package/skills/loader/SKILL.md +2 -0
- package/skills/migrate-react-router/SKILL.md +4 -2
- package/skills/mime-routes/SKILL.md +1 -1
- package/skills/prerender/SKILL.md +2 -0
- package/skills/rango/SKILL.md +12 -11
- package/skills/response-routes/SKILL.md +2 -2
- package/skills/route/SKILL.md +4 -0
- package/skills/router-setup/SKILL.md +3 -0
- package/skills/scripts/SKILL.md +179 -0
- package/skills/testing/SKILL.md +1 -1
- package/skills/testing/bindings.md +20 -6
- package/skills/testing/cache-prerender.md +5 -2
- package/skills/testing/client-components.md +2 -0
- package/skills/testing/e2e-parity.md +1 -1
- package/skills/testing/flight.md +8 -9
- package/skills/testing/render-handler.md +1 -1
- package/skills/testing/response-routes.md +1 -1
- package/skills/testing/server-actions.md +11 -11
- package/skills/testing/setup.md +3 -0
- package/skills/typesafety/SKILL.md +3 -2
- package/skills/use-cache/SKILL.md +10 -9
- package/src/browser/event-controller.ts +109 -2
- package/src/browser/partial-update.ts +12 -0
- package/src/browser/prefetch/cache.ts +17 -0
- package/src/browser/prefetch/fetch.ts +69 -2
- package/src/browser/react/Link.tsx +30 -5
- package/src/browser/react/NavigationProvider.tsx +12 -2
- package/src/browser/react/location-state-shared.ts +14 -2
- package/src/browser/react/use-href.tsx +8 -1
- package/src/browser/react/use-link-status.ts +23 -2
- package/src/browser/response-adapter.ts +14 -3
- package/src/browser/rsc-router.tsx +3 -0
- package/src/browser/scroll-restoration.ts +8 -3
- package/src/browser/server-action-bridge.ts +46 -11
- package/src/browser/types.ts +6 -0
- package/src/build/generate-route-types.ts +0 -1
- package/src/build/route-trie.ts +33 -9
- package/src/build/route-types/include-resolution.ts +7 -1
- package/src/build/route-types/router-processing.ts +0 -6
- package/src/build/route-types/source-scan.ts +105 -7
- package/src/cache/cache-policy.ts +42 -8
- package/src/cache/cache-runtime.ts +65 -5
- package/src/cache/cache-scope.ts +71 -11
- package/src/cache/cache-tag.ts +7 -2
- package/src/cache/cf/cf-base64.ts +33 -0
- package/src/cache/cf/cf-cache-constants.ts +127 -0
- package/src/cache/cf/cf-cache-store.ts +85 -613
- package/src/cache/cf/cf-cache-types.ts +349 -0
- package/src/cache/cf/cf-kv-utils.ts +46 -0
- package/src/cache/cf/cf-tag-marker-memo.ts +105 -0
- package/src/cache/document-cache.ts +11 -0
- package/src/cache/handle-snapshot.ts +8 -1
- package/src/cache/profile-registry.ts +25 -1
- package/src/cache/segment-codec.ts +9 -1
- package/src/cache/types.ts +4 -0
- package/src/client.rsc.tsx +38 -0
- package/src/client.tsx +11 -0
- package/src/components/DefaultDocument.tsx +8 -2
- package/src/context-var.ts +1 -1
- package/src/decode-loader-results.ts +7 -1
- package/src/escape-script.ts +52 -0
- package/src/handles/MetaTags.tsx +56 -5
- package/src/handles/Scripts.tsx +183 -0
- package/src/handles/breadcrumbs.ts +29 -11
- package/src/handles/is-thenable.ts +19 -0
- package/src/handles/meta.ts +46 -0
- package/src/handles/script.ts +244 -0
- package/src/host/cookie-handler.ts +7 -3
- package/src/host/pattern-matcher.ts +16 -2
- package/src/index.rsc.ts +5 -0
- package/src/index.ts +5 -0
- package/src/response-utils.ts +25 -0
- package/src/route-definition/dsl-helpers.ts +7 -0
- package/src/route-definition/redirect.ts +1 -2
- package/src/router/content-negotiation.ts +58 -10
- package/src/router/intercept-resolution.ts +9 -0
- package/src/router/match-middleware/cache-store.ts +10 -1
- package/src/router/middleware.ts +10 -3
- package/src/router/pattern-matching.ts +25 -23
- package/src/router/prefetch-cache-ttl.ts +51 -0
- package/src/router/router-interfaces.ts +7 -0
- package/src/router/router-options.ts +23 -0
- package/src/router/segment-resolution/fresh.ts +10 -0
- package/src/router/segment-resolution/helpers.ts +35 -1
- package/src/router/segment-resolution/loader-cache.ts +10 -6
- package/src/router/segment-resolution/revalidation.ts +6 -0
- package/src/router/segment-resolution.ts +1 -0
- package/src/router/trie-matching.ts +14 -9
- package/src/router.ts +18 -10
- package/src/rsc/handler.ts +52 -13
- package/src/rsc/helpers.ts +7 -1
- package/src/rsc/index.ts +1 -4
- package/src/rsc/loader-fetch.ts +107 -37
- package/src/rsc/progressive-enhancement.ts +18 -6
- package/src/rsc/response-cache-serve.ts +238 -0
- package/src/rsc/response-route-handler.ts +16 -133
- package/src/rsc/rsc-rendering.ts +13 -4
- package/src/rsc/server-action.ts +52 -6
- package/src/rsc/types.ts +7 -0
- package/src/search-params.ts +24 -5
- package/src/segment-loader-promise.ts +17 -2
- package/src/server/loader-registry.ts +16 -18
- package/src/server/request-context.ts +47 -20
- package/src/testing/dispatch.ts +108 -25
- package/src/testing/flight.ts +25 -0
- package/src/testing/internal/context.ts +25 -2
- package/src/testing/render-handler.ts +3 -1
- package/src/testing/render-route.tsx +15 -0
- package/src/testing/run-loader.ts +10 -3
- package/src/theme/ThemeProvider.tsx +20 -6
- package/src/theme/ThemeScript.tsx +7 -3
- package/src/theme/constants.ts +54 -3
- package/src/theme/theme-script.ts +22 -7
- package/src/types/request-scope.ts +8 -3
- package/src/vite/plugins/cjs-to-esm.ts +8 -1
- package/src/vite/plugins/expose-id-utils.ts +10 -1
- package/src/vite/plugins/expose-ids/handler-transform.ts +5 -16
- package/src/vite/plugins/expose-ids/loader-transform.ts +12 -5
- package/src/vite/plugins/expose-ids/router-transform.ts +6 -1
- package/src/vite/plugins/expose-internal-ids.ts +0 -1
- package/src/vite/plugins/version-plugin.ts +5 -17
- package/src/vite/plugins/virtual-entries.ts +12 -2
- package/src/vite/rango.ts +15 -6
- package/src/vite/utils/ast-handler-extract.ts +11 -4
- package/src/vite/utils/directive-prologue.ts +40 -0
- package/src/vite/utils/prerender-utils.ts +17 -2
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the prefetch cache TTL once, at router init, into the three derived
|
|
3
|
+
* values the rest of the router consumes: seconds (for the Cache-Control
|
|
4
|
+
* max-age), milliseconds (for the client-side in-memory cache), and the
|
|
5
|
+
* Cache-Control header string (or false when caching is disabled).
|
|
6
|
+
*
|
|
7
|
+
* `false` disables prefetch caching (seconds 0). A non-finite input
|
|
8
|
+
* (NaN/Infinity, e.g. from an env-derived number that failed to parse) is
|
|
9
|
+
* treated as the default rather than producing a malformed
|
|
10
|
+
* `Cache-Control: max-age=NaN` header; CDNs/browsers ignore such a directive,
|
|
11
|
+
* which would silently disable caching on EVERY prefetch response. Negative
|
|
12
|
+
* finite values clamp to 0 (disabled).
|
|
13
|
+
*
|
|
14
|
+
* Policy note: this is the prefetch-TTL policy specifically. Other finite-number
|
|
15
|
+
* guards in the codebase deliberately differ — defer.ts treats Infinity as an
|
|
16
|
+
* intentional disable, and profile-registry.ts THROWS on non-finite/negative ttl
|
|
17
|
+
* at config time. They are NOT the same guard, so don't unify them.
|
|
18
|
+
*/
|
|
19
|
+
export interface ResolvedPrefetchCacheTTL {
|
|
20
|
+
/** TTL in seconds for the Cache-Control max-age directive. */
|
|
21
|
+
seconds: number;
|
|
22
|
+
/** TTL in milliseconds for the client-side in-memory prefetch cache. */
|
|
23
|
+
ms: number;
|
|
24
|
+
/** Cache-Control header value, or false when caching is disabled. */
|
|
25
|
+
cacheControl: string | false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const DEFAULT_PREFETCH_CACHE_TTL_SECONDS = 300;
|
|
29
|
+
|
|
30
|
+
export function resolvePrefetchCacheTTL(
|
|
31
|
+
rawTTL: number | false | undefined,
|
|
32
|
+
): ResolvedPrefetchCacheTTL {
|
|
33
|
+
let seconds: number;
|
|
34
|
+
if (rawTTL === false) {
|
|
35
|
+
seconds = 0;
|
|
36
|
+
} else if (rawTTL === undefined) {
|
|
37
|
+
seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
|
|
38
|
+
} else if (typeof rawTTL === "number" && Number.isFinite(rawTTL)) {
|
|
39
|
+
seconds = Math.max(0, Math.floor(rawTTL));
|
|
40
|
+
} else {
|
|
41
|
+
// Non-finite (NaN/Infinity): fall back to the default rather than emit a
|
|
42
|
+
// malformed max-age=NaN/Infinity that CDNs and browsers reject.
|
|
43
|
+
seconds = DEFAULT_PREFETCH_CACHE_TTL_SECONDS;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
seconds,
|
|
48
|
+
ms: seconds * 1000,
|
|
49
|
+
cacheControl: seconds === 0 ? false : `private, max-age=${seconds}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -315,6 +315,13 @@ export interface RangoInternal<
|
|
|
315
315
|
*/
|
|
316
316
|
readonly warmupEnabled: boolean;
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* Whether the client hydrates inside React.StrictMode. Resolved from
|
|
320
|
+
* createRouter({ strictMode }) (default true) and shipped to the client in
|
|
321
|
+
* the initial payload metadata.
|
|
322
|
+
*/
|
|
323
|
+
readonly strictMode: boolean;
|
|
324
|
+
|
|
318
325
|
/**
|
|
319
326
|
* Whether router-wide performance debugging is enabled.
|
|
320
327
|
* Used by the request handler to create metrics before middleware runs.
|
|
@@ -523,6 +523,29 @@ export interface RangoOptions<TEnv = any> {
|
|
|
523
523
|
*/
|
|
524
524
|
warmup?: boolean;
|
|
525
525
|
|
|
526
|
+
/**
|
|
527
|
+
* Wrap the hydrated client tree in `React.StrictMode`.
|
|
528
|
+
*
|
|
529
|
+
* The Rango browser entry hydrates the app inside `<React.StrictMode>` by
|
|
530
|
+
* default. StrictMode double-invokes render and (in development) mounts,
|
|
531
|
+
* unmounts, then remounts every effect to surface impure renders and missing
|
|
532
|
+
* effect cleanup. Production builds treat StrictMode as a no-op, so this flag
|
|
533
|
+
* only changes development behavior in a normal app.
|
|
534
|
+
*
|
|
535
|
+
* Set to `false` to hydrate without the StrictMode wrapper. The main reason to
|
|
536
|
+
* opt out is to isolate StrictMode's intentional double-render/double-effect
|
|
537
|
+
* from genuine re-renders when measuring client-hook stability — with
|
|
538
|
+
* StrictMode off, render counts are exact in development too.
|
|
539
|
+
*
|
|
540
|
+
* The value is resolved server-side at router creation and shipped to the
|
|
541
|
+
* client in the initial payload metadata; the browser entry reads it once at
|
|
542
|
+
* hydration. Changing it does not affect the SSR HTML (StrictMode emits no
|
|
543
|
+
* DOM), so toggling it never causes a hydration mismatch.
|
|
544
|
+
*
|
|
545
|
+
* @default true
|
|
546
|
+
*/
|
|
547
|
+
strictMode?: boolean;
|
|
548
|
+
|
|
526
549
|
/**
|
|
527
550
|
* Shorthand timeout (ms) applied to both action execution and render start.
|
|
528
551
|
* Does NOT apply to streamIdleMs.
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
resolveLayoutComponent,
|
|
27
27
|
resolveWithErrorBoundary,
|
|
28
28
|
warnOnStreamedResponse,
|
|
29
|
+
buildLoaderErrorContext,
|
|
29
30
|
} from "./helpers.js";
|
|
30
31
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
31
32
|
import { getRouterContext } from "../router-context.js";
|
|
@@ -59,6 +60,13 @@ export async function resolveLoaders<TEnv>(
|
|
|
59
60
|
const hasLoading = "loading" in entry && entry.loading !== undefined;
|
|
60
61
|
const loadingDisabled = hasLoading && entry.loading === false;
|
|
61
62
|
|
|
63
|
+
// Error context for wrapLoaderPromise: without it, a throwing DSL loader never
|
|
64
|
+
// fires createRouter({ onError }) (phase "loader") nor emits the loader.error
|
|
65
|
+
// telemetry event — wrapLoaderPromise only builds the onError/telemetry path
|
|
66
|
+
// when errorContext is supplied. Built from ctx so the live render path reports
|
|
67
|
+
// loader failures the same way handlers/actions/routing/fetchable-loaders do.
|
|
68
|
+
const errorContext = buildLoaderErrorContext(ctx);
|
|
69
|
+
|
|
62
70
|
if (!loadingDisabled) {
|
|
63
71
|
// Streaming loaders: promises kick off now, settle during RSC serialization.
|
|
64
72
|
const segments = loaderEntries.map((loaderEntry, i) => {
|
|
@@ -79,6 +87,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
79
87
|
entry,
|
|
80
88
|
segmentId,
|
|
81
89
|
ctx.pathname,
|
|
90
|
+
errorContext,
|
|
82
91
|
),
|
|
83
92
|
belongsToRoute,
|
|
84
93
|
};
|
|
@@ -107,6 +116,7 @@ export async function resolveLoaders<TEnv>(
|
|
|
107
116
|
entry,
|
|
108
117
|
segmentId,
|
|
109
118
|
ctx.pathname,
|
|
119
|
+
errorContext,
|
|
110
120
|
);
|
|
111
121
|
return { wrapped, segmentId };
|
|
112
122
|
});
|
|
@@ -19,7 +19,12 @@ import {
|
|
|
19
19
|
import { getRequestContext } from "../../server/request-context.js";
|
|
20
20
|
import { DefaultErrorFallback } from "../../default-error-boundary.js";
|
|
21
21
|
import type { EntryData } from "../../server/context";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
ResolvedSegment,
|
|
24
|
+
ErrorInfo,
|
|
25
|
+
HandlerContext,
|
|
26
|
+
InternalHandlerContext,
|
|
27
|
+
} from "../../types";
|
|
23
28
|
import type { SegmentResolutionDeps } from "../types.js";
|
|
24
29
|
import { debugLog } from "../logging.js";
|
|
25
30
|
import { tryStaticLookup } from "./static-store.js";
|
|
@@ -27,6 +32,35 @@ import { observeHandler } from "../instrument.js";
|
|
|
27
32
|
import type { TelemetrySink } from "../telemetry.js";
|
|
28
33
|
import { resolveSink, safeEmit, getRequestId } from "../telemetry.js";
|
|
29
34
|
|
|
35
|
+
/** The errorContext shape wrapLoaderPromise expects as its 5th argument. */
|
|
36
|
+
type LoaderErrorContext<TEnv> = NonNullable<
|
|
37
|
+
Parameters<SegmentResolutionDeps<TEnv>["wrapLoaderPromise"]>[4]
|
|
38
|
+
>;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build the errorContext passed to wrapLoaderPromise so a throwing DSL loader
|
|
42
|
+
* fires createRouter({ onError }) (phase "loader") and emits the loader.error
|
|
43
|
+
* telemetry event. wrapLoaderPromise only wires the onError/telemetry path when
|
|
44
|
+
* this 5th argument is present; every real call site previously omitted it, so
|
|
45
|
+
* loaders were the one phase whose failures were silently dropped (handlers,
|
|
46
|
+
* actions, routing, rendering, and fetchable loaders all reported correctly).
|
|
47
|
+
*
|
|
48
|
+
* The fields come off the handler context, which already carries the request,
|
|
49
|
+
* url, params, env, and (on the internal shape) the matched route name.
|
|
50
|
+
*/
|
|
51
|
+
export function buildLoaderErrorContext<TEnv>(
|
|
52
|
+
ctx: HandlerContext<any, TEnv>,
|
|
53
|
+
): LoaderErrorContext<TEnv> {
|
|
54
|
+
const internal = ctx as InternalHandlerContext<any, TEnv>;
|
|
55
|
+
return {
|
|
56
|
+
request: ctx.request,
|
|
57
|
+
url: ctx.url,
|
|
58
|
+
routeKey: internal._routeName,
|
|
59
|
+
params: ctx.params as Record<string, string>,
|
|
60
|
+
env: ctx.env,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
30
64
|
// ---------------------------------------------------------------------------
|
|
31
65
|
// Handler result processing
|
|
32
66
|
// ---------------------------------------------------------------------------
|
|
@@ -147,12 +147,6 @@ export function resolveLoaderData<TEnv>(
|
|
|
147
147
|
|
|
148
148
|
const loaderId = loaderEntry.loader.$$id;
|
|
149
149
|
|
|
150
|
-
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
151
|
-
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
152
|
-
const swr = swrWindow || undefined;
|
|
153
|
-
const tags = resolveTags(loaderEntry);
|
|
154
|
-
recordRequestTags(tags);
|
|
155
|
-
|
|
156
150
|
// A handler that later awaits this same loader via ctx.use(loader) must get
|
|
157
151
|
// THIS memoized promise, not a fresh execution. Rather than rebind ctx.use
|
|
158
152
|
// once per cached loader (O(N) chained wrappers + a synchronous
|
|
@@ -188,6 +182,16 @@ export function resolveLoaderData<TEnv>(
|
|
|
188
182
|
const existing = overrides.get(loaderId);
|
|
189
183
|
if (existing) return existing;
|
|
190
184
|
|
|
185
|
+
// Compute ttl/swr/tags only AFTER the dedup short-circuit: a deduped second
|
|
186
|
+
// resolution of the same loaderId (the orphan-layout inheritance path) must
|
|
187
|
+
// not re-run the user tags() callback. These values are only consumed inside
|
|
188
|
+
// the read-through below, so they belong here, past the dedup gate.
|
|
189
|
+
const ttl = resolveTtl(options.ttl, store.defaults, DEFAULT_ROUTE_TTL);
|
|
190
|
+
const swrWindow = resolveSwrWindow(options.swr, store.defaults);
|
|
191
|
+
const swr = swrWindow || undefined;
|
|
192
|
+
const tags = resolveTags(loaderEntry);
|
|
193
|
+
recordRequestTags(tags);
|
|
194
|
+
|
|
191
195
|
const dataPromise = (async () => {
|
|
192
196
|
const codec = await getCodec();
|
|
193
197
|
const key = await resolveLoaderKey(
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
tryStaticSlot,
|
|
41
41
|
resolveLayoutComponent,
|
|
42
42
|
resolveWithErrorBoundary,
|
|
43
|
+
buildLoaderErrorContext,
|
|
43
44
|
} from "./helpers.js";
|
|
44
45
|
import { applyViewTransitionDefault } from "./view-transition-default.js";
|
|
45
46
|
import { getRouterContext } from "../router-context.js";
|
|
@@ -199,6 +200,10 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
199
200
|
),
|
|
200
201
|
);
|
|
201
202
|
|
|
203
|
+
// Partial (revalidation) render path: a throwing DSL loader must still fire
|
|
204
|
+
// onError/loader.error. isPartial flags the reporting phase accordingly.
|
|
205
|
+
const errorContext = { ...buildLoaderErrorContext(ctx), isPartial: true };
|
|
206
|
+
|
|
202
207
|
const loadersToRun = revalidationChecks.filter((c) => c.shouldRun);
|
|
203
208
|
const segments: ResolvedSegment[] = loadersToRun.map(
|
|
204
209
|
({ loaderEntry, loader, segmentId, index }) => ({
|
|
@@ -216,6 +221,7 @@ export async function resolveLoadersWithRevalidation<TEnv>(
|
|
|
216
221
|
entry,
|
|
217
222
|
segmentId,
|
|
218
223
|
ctx.pathname,
|
|
224
|
+
errorContext,
|
|
219
225
|
),
|
|
220
226
|
belongsToRoute,
|
|
221
227
|
}),
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
* Falls back to null when no match is found (caller uses regex fallback).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
TrieNode,
|
|
10
|
+
TrieLeaf,
|
|
11
|
+
NegotiateVariant,
|
|
12
|
+
} from "../build/route-trie.js";
|
|
9
13
|
import { safeDecodeURIComponent } from "./url-params.js";
|
|
10
14
|
|
|
11
15
|
export interface TrieMatchResult {
|
|
@@ -24,7 +28,7 @@ export interface TrieMatchResult {
|
|
|
24
28
|
/** Response type for non-RSC routes (json, text, image, any) */
|
|
25
29
|
responseType?: string;
|
|
26
30
|
/** Negotiate variants: response-type routes sharing this path */
|
|
27
|
-
negotiateVariants?:
|
|
31
|
+
negotiateVariants?: NegotiateVariant[];
|
|
28
32
|
/** RSC-first: RSC route was defined before response-type variants */
|
|
29
33
|
rscFirst?: true;
|
|
30
34
|
}
|
|
@@ -231,7 +235,13 @@ function walkTrie(
|
|
|
231
235
|
}
|
|
232
236
|
}
|
|
233
237
|
|
|
234
|
-
|
|
238
|
+
// A required single-segment param captures 1+ chars (the regex matcher emits
|
|
239
|
+
// `([^/]+)`), so an empty path segment from a double slash (`/a//b`) must NOT
|
|
240
|
+
// bind `:s` to "". Reject it here so the trie matches the regex contract and
|
|
241
|
+
// a malformed URL 404s instead of running the handler with an empty param.
|
|
242
|
+
// The suffix-param branch above already requires `segment.length > suffix`,
|
|
243
|
+
// and node.w may legitimately be empty, so only this branch needs the guard.
|
|
244
|
+
if (node.p && segment !== "") {
|
|
235
245
|
paramValues.push(segment);
|
|
236
246
|
const result = walkTrie(node.p.c, segments, index + 1, paramValues);
|
|
237
247
|
paramValues.pop();
|
|
@@ -256,12 +266,7 @@ function walkTrie(
|
|
|
256
266
|
|
|
257
267
|
function joinRemainingSegments(segments: string[], start: number): string {
|
|
258
268
|
if (start >= segments.length) return "";
|
|
259
|
-
|
|
260
|
-
let rest = segments[start]!;
|
|
261
|
-
for (let i = start + 1; i < segments.length; i++) {
|
|
262
|
-
rest += "/" + segments[i]!;
|
|
263
|
-
}
|
|
264
|
-
return rest;
|
|
269
|
+
return segments.slice(start).join("/");
|
|
265
270
|
}
|
|
266
271
|
|
|
267
272
|
/**
|
package/src/router.ts
CHANGED
|
@@ -110,6 +110,7 @@ import {
|
|
|
110
110
|
renderStaticSegment as _renderStaticSegment,
|
|
111
111
|
} from "./router/prerender-match.js";
|
|
112
112
|
import { resolveStateCookieName } from "./router/state-cookie-name.js";
|
|
113
|
+
import { resolvePrefetchCacheTTL } from "./router/prefetch-cache-ttl.js";
|
|
113
114
|
|
|
114
115
|
// Re-export public types and values from extracted modules
|
|
115
116
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
@@ -161,6 +162,7 @@ export function createRouter<TEnv = any>(
|
|
|
161
162
|
originCheck: originCheckOption,
|
|
162
163
|
viewTransition: viewTransitionOption = "auto",
|
|
163
164
|
debugCacheSignal: debugCacheSignalOption = false,
|
|
165
|
+
strictMode: strictModeOption = true,
|
|
164
166
|
} = options;
|
|
165
167
|
|
|
166
168
|
// Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
|
|
@@ -230,21 +232,24 @@ export function createRouter<TEnv = any>(
|
|
|
230
232
|
routerId,
|
|
231
233
|
);
|
|
232
234
|
|
|
233
|
-
// Resolve prefetch cache TTL (default: 300 seconds / 5 minutes)
|
|
234
|
-
//
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const prefetchCacheTTL =
|
|
235
|
+
// Resolve prefetch cache TTL (default: 300 seconds / 5 minutes). Clamps to a
|
|
236
|
+
// non-negative integer and guards non-finite (NaN/Infinity) inputs so a
|
|
237
|
+
// malformed `Cache-Control: max-age=NaN` can never reach the wire.
|
|
238
|
+
const resolvedPrefetchCacheTTL = resolvePrefetchCacheTTL(
|
|
239
|
+
prefetchCacheTTLOption,
|
|
240
|
+
);
|
|
241
|
+
const prefetchCacheTTL = resolvedPrefetchCacheTTL.ms;
|
|
240
242
|
const prefetchCacheControl: string | false =
|
|
241
|
-
|
|
242
|
-
? false
|
|
243
|
-
: `private, max-age=${prefetchCacheTTLSeconds}`;
|
|
243
|
+
resolvedPrefetchCacheTTL.cacheControl;
|
|
244
244
|
|
|
245
245
|
// Resolve warmup enabled flag (default: true)
|
|
246
246
|
const warmupEnabled = warmupOption !== false;
|
|
247
247
|
|
|
248
|
+
// Resolve StrictMode flag (default: true). Shipped to the client in payload
|
|
249
|
+
// metadata; the browser entry reads it once to decide whether to wrap the
|
|
250
|
+
// hydrated tree in React.StrictMode.
|
|
251
|
+
const strictMode = strictModeOption !== false;
|
|
252
|
+
|
|
248
253
|
// Resolve theme config (null if theme not enabled)
|
|
249
254
|
const resolvedThemeConfig = themeOption
|
|
250
255
|
? resolveThemeConfig(themeOption)
|
|
@@ -971,6 +976,9 @@ export function createRouter<TEnv = any>(
|
|
|
971
976
|
// Expose warmup enabled flag for handler and client
|
|
972
977
|
warmupEnabled,
|
|
973
978
|
|
|
979
|
+
// Expose StrictMode flag for the initial-render payload metadata
|
|
980
|
+
strictMode,
|
|
981
|
+
|
|
974
982
|
// Expose router-wide performance debugging for request-level metrics setup
|
|
975
983
|
debugPerformance,
|
|
976
984
|
|
package/src/rsc/handler.ts
CHANGED
|
@@ -13,7 +13,6 @@ import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
|
13
13
|
import {
|
|
14
14
|
runWithRequestContext,
|
|
15
15
|
setRequestContextParams,
|
|
16
|
-
requireRequestContext,
|
|
17
16
|
getRequestContext,
|
|
18
17
|
_getRequestContext,
|
|
19
18
|
createRequestContext,
|
|
@@ -32,7 +31,10 @@ import {
|
|
|
32
31
|
buildRouteMiddlewareEntries,
|
|
33
32
|
} from "./helpers.js";
|
|
34
33
|
import { guardOutgoingRedirect } from "./redirect-guard.js";
|
|
35
|
-
import {
|
|
34
|
+
import {
|
|
35
|
+
isWebSocketUpgradeResponse,
|
|
36
|
+
appendVaryAccept,
|
|
37
|
+
} from "../response-utils.js";
|
|
36
38
|
import {
|
|
37
39
|
handleResponseRoute,
|
|
38
40
|
type ResponseRouteMatch,
|
|
@@ -294,7 +296,17 @@ export function createRSCHandler<
|
|
|
294
296
|
...(locationState && { locationState }),
|
|
295
297
|
},
|
|
296
298
|
};
|
|
297
|
-
const rscStream = renderToReadableStream<RscPayload>(redirectPayload
|
|
299
|
+
const rscStream = renderToReadableStream<RscPayload>(redirectPayload, {
|
|
300
|
+
onError: (error: unknown) => {
|
|
301
|
+
const reqCtx = _getRequestContext<TEnv>();
|
|
302
|
+
if (!reqCtx) return;
|
|
303
|
+
callOnError(error, "rendering", {
|
|
304
|
+
request: reqCtx.request,
|
|
305
|
+
url: reqCtx.url,
|
|
306
|
+
env: reqCtx.env,
|
|
307
|
+
});
|
|
308
|
+
},
|
|
309
|
+
});
|
|
298
310
|
return createResponseWithMergedHeaders(rscStream, {
|
|
299
311
|
status: 200,
|
|
300
312
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
@@ -762,11 +774,11 @@ export function createRSCHandler<
|
|
|
762
774
|
nonce: string | undefined,
|
|
763
775
|
): Promise<Response> {
|
|
764
776
|
// Common setup
|
|
765
|
-
const handleStore =
|
|
777
|
+
const handleStore = getRequestContext()._handleStore;
|
|
766
778
|
|
|
767
779
|
// Wire up error reporting for late streaming-handle failures
|
|
768
780
|
handleStore.onError = (error: Error) => {
|
|
769
|
-
const reqCtx =
|
|
781
|
+
const reqCtx = getRequestContext();
|
|
770
782
|
callOnError(error, "handler", {
|
|
771
783
|
request,
|
|
772
784
|
url,
|
|
@@ -790,7 +802,7 @@ export function createRSCHandler<
|
|
|
790
802
|
// instead of calling resolveRoute again.
|
|
791
803
|
if (plan.mode !== "redirect") {
|
|
792
804
|
setRequestContextParams(plan.route.params, plan.route.routeKey);
|
|
793
|
-
|
|
805
|
+
getRequestContext()._classifiedRoute = plan.route;
|
|
794
806
|
}
|
|
795
807
|
|
|
796
808
|
const routeReverse = createReverseFunction(getRequiredRouteMap());
|
|
@@ -831,7 +843,9 @@ export function createRSCHandler<
|
|
|
831
843
|
}
|
|
832
844
|
const response = responseOutcome.result;
|
|
833
845
|
if (plan.negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
834
|
-
|
|
846
|
+
// handleResponseRoute (callHandlerWithVary) already appends Vary: Accept
|
|
847
|
+
// for negotiated responses; dedup so we don't emit Vary: Accept, Accept.
|
|
848
|
+
appendVaryAccept(response);
|
|
835
849
|
}
|
|
836
850
|
return response;
|
|
837
851
|
}
|
|
@@ -839,14 +853,28 @@ export function createRSCHandler<
|
|
|
839
853
|
// SSR setup: kick off in parallel for modes that need HTML rendering.
|
|
840
854
|
// Placed after response-route short-circuit so response/mime routes
|
|
841
855
|
// never pay for SSR work.
|
|
842
|
-
|
|
856
|
+
//
|
|
857
|
+
// Only kick off when the request will actually render HTML, so the
|
|
858
|
+
// eager loadSSRModule() + user resolveStreaming() are not started (and
|
|
859
|
+
// never consumed) for a request that returns an RSC stream — that wasted
|
|
860
|
+
// work also leaves an orphaned Promise.all that can reject (D7). PE form
|
|
861
|
+
// submissions always render HTML (handleProgressiveEnhancement renders via
|
|
862
|
+
// getSSRSetup regardless of Accept). For full/partial-render and action,
|
|
863
|
+
// the render-time HTML decision is exactly !isRscRequest — mayNeedSSR is
|
|
864
|
+
// the coarse transport pre-filter, isRscRequest is the precise Accept call
|
|
865
|
+
// (it, unlike mayNeedSSR, treats a MISSING Accept as RSC). Both must pass.
|
|
866
|
+
const willRenderHtml =
|
|
867
|
+
plan.mode === "pe-render" ||
|
|
868
|
+
(mayNeedSSR(request, url) &&
|
|
869
|
+
!isRscRequest(request, url, plan.mode === "partial-render"));
|
|
870
|
+
if (plan.mode !== "loader" && willRenderHtml) {
|
|
843
871
|
variables[SSR_SETUP_VAR] = startSSRSetup(
|
|
844
872
|
handlerCtx,
|
|
845
873
|
request,
|
|
846
874
|
env,
|
|
847
875
|
url,
|
|
848
876
|
router.debugPerformance
|
|
849
|
-
? () =>
|
|
877
|
+
? () => getRequestContext()._metricsStore
|
|
850
878
|
: undefined,
|
|
851
879
|
);
|
|
852
880
|
}
|
|
@@ -989,7 +1017,7 @@ export function createRSCHandler<
|
|
|
989
1017
|
url: URL,
|
|
990
1018
|
variables: Record<string, any>,
|
|
991
1019
|
nonce: string | undefined,
|
|
992
|
-
handleStore: ReturnType<typeof
|
|
1020
|
+
handleStore: ReturnType<typeof getRequestContext>["_handleStore"],
|
|
993
1021
|
isPartial: boolean,
|
|
994
1022
|
actionContinuation?: ActionContinuation,
|
|
995
1023
|
): Promise<Response> {
|
|
@@ -1017,7 +1045,9 @@ export function createRSCHandler<
|
|
|
1017
1045
|
);
|
|
1018
1046
|
}
|
|
1019
1047
|
if (negotiated && !isWebSocketUpgradeResponse(response)) {
|
|
1020
|
-
response
|
|
1048
|
+
// handleRscRendering bakes `accept` into the RSC response's Vary list;
|
|
1049
|
+
// dedup so the negotiated append does not list accept twice.
|
|
1050
|
+
appendVaryAccept(response);
|
|
1021
1051
|
}
|
|
1022
1052
|
return response;
|
|
1023
1053
|
} catch (error) {
|
|
@@ -1092,14 +1122,23 @@ export function createRSCHandler<
|
|
|
1092
1122
|
segments: [notFoundSegment],
|
|
1093
1123
|
matched: [],
|
|
1094
1124
|
diff: [],
|
|
1125
|
+
// Shape parity with buildFullPayload (rsc-rendering.ts): the 404
|
|
1126
|
+
// payload carries params/resolvedIds/prefetchCacheTTL the same way a
|
|
1127
|
+
// matched full render does. resolvedIds mirrors the rendered segment
|
|
1128
|
+
// list (the single notFound segment) like the error-boundary path
|
|
1129
|
+
// (match-api.ts) does for its boundary segment.
|
|
1130
|
+
resolvedIds: [notFoundSegment.id],
|
|
1131
|
+
params: {},
|
|
1095
1132
|
isPartial: false,
|
|
1096
1133
|
rootLayout: router.rootLayout,
|
|
1097
1134
|
handles: handleStore.stream(),
|
|
1098
1135
|
version,
|
|
1136
|
+
prefetchCacheTTL: router.prefetchCacheTTL,
|
|
1099
1137
|
stateCookieName: router.resolvedStateCookieName,
|
|
1100
1138
|
themeConfig: router.themeConfig,
|
|
1101
1139
|
warmupEnabled: router.warmupEnabled,
|
|
1102
|
-
|
|
1140
|
+
strictMode: router.strictMode,
|
|
1141
|
+
initialTheme: getRequestContext().theme,
|
|
1103
1142
|
},
|
|
1104
1143
|
};
|
|
1105
1144
|
|
|
@@ -1126,7 +1165,7 @@ export function createRSCHandler<
|
|
|
1126
1165
|
request,
|
|
1127
1166
|
env,
|
|
1128
1167
|
url,
|
|
1129
|
-
|
|
1168
|
+
getRequestContext()._metricsStore,
|
|
1130
1169
|
);
|
|
1131
1170
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
1132
1171
|
nonce,
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -65,8 +65,14 @@ function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
|
65
65
|
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
66
66
|
* iteration prevents re-entrant registrations from double-firing and matches
|
|
67
67
|
* the contract that each callback runs at most once per request.
|
|
68
|
+
*
|
|
69
|
+
* Exported so the testing primitives' buildRunResponse can reuse the exact
|
|
70
|
+
* production drain (swap-before-iterate + external-redirect brand preservation)
|
|
71
|
+
* rather than maintain a hand-synced copy. This module is plugin-rsc-free on its
|
|
72
|
+
* eager graph (dispatch.ts already statically imports from here in the same
|
|
73
|
+
* testing barrel), so importing it adds nothing to the unit runner's graph.
|
|
68
74
|
*/
|
|
69
|
-
function drainOnResponseCallbacks(
|
|
75
|
+
export function drainOnResponseCallbacks(
|
|
70
76
|
ctx: RequestContext,
|
|
71
77
|
response: Response,
|
|
72
78
|
): Response {
|
package/src/rsc/index.ts
CHANGED
|
@@ -30,7 +30,4 @@ export type {
|
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
|
|
32
32
|
// Re-export request context utilities for server-side access to env/request/params
|
|
33
|
-
export {
|
|
34
|
-
getRequestContext,
|
|
35
|
-
requireRequestContext,
|
|
36
|
-
} from "../server/request-context.js";
|
|
33
|
+
export { getRequestContext } from "../server/request-context.js";
|