@rangojs/router 0.0.0-experimental.10 → 0.0.0-experimental.100
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 +9 -0
- package/README.md +1037 -4
- package/dist/bin/rango.js +1619 -157
- package/dist/vite/index.js +5762 -2301
- package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/package.json +71 -63
- package/skills/breadcrumbs/SKILL.md +252 -0
- package/skills/cache-guide/SKILL.md +294 -0
- package/skills/caching/SKILL.md +93 -23
- package/skills/composability/SKILL.md +172 -0
- package/skills/debug-manifest/SKILL.md +12 -8
- package/skills/document-cache/SKILL.md +18 -16
- package/skills/fonts/SKILL.md +6 -4
- package/skills/handler-use/SKILL.md +364 -0
- package/skills/hooks/SKILL.md +367 -71
- package/skills/host-router/SKILL.md +218 -0
- package/skills/i18n/SKILL.md +276 -0
- package/skills/intercept/SKILL.md +176 -8
- package/skills/layout/SKILL.md +124 -3
- package/skills/links/SKILL.md +304 -25
- package/skills/loader/SKILL.md +474 -47
- package/skills/middleware/SKILL.md +207 -37
- package/skills/migrate-nextjs/SKILL.md +562 -0
- package/skills/migrate-react-router/SKILL.md +769 -0
- package/skills/mime-routes/SKILL.md +15 -11
- package/skills/parallel/SKILL.md +272 -1
- package/skills/prerender/SKILL.md +467 -65
- package/skills/rango/SKILL.md +89 -21
- package/skills/response-routes/SKILL.md +152 -91
- package/skills/route/SKILL.md +305 -14
- package/skills/router-setup/SKILL.md +210 -32
- package/skills/server-actions/SKILL.md +739 -0
- package/skills/streams-and-websockets/SKILL.md +283 -0
- package/skills/theme/SKILL.md +9 -8
- package/skills/typesafety/SKILL.md +333 -86
- package/skills/use-cache/SKILL.md +324 -0
- package/skills/view-transitions/SKILL.md +212 -0
- package/src/__internal.ts +102 -4
- package/src/bin/rango.ts +312 -15
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/action-response-classifier.ts +99 -0
- package/src/browser/app-shell.ts +52 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +136 -68
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +52 -0
- package/src/browser/link-interceptor.ts +24 -4
- package/src/browser/logging.ts +55 -0
- package/src/browser/merge-segment-loaders.ts +20 -12
- package/src/browser/navigation-bridge.ts +374 -561
- package/src/browser/navigation-client.ts +228 -70
- package/src/browser/navigation-store.ts +97 -55
- package/src/browser/navigation-transaction.ts +297 -0
- package/src/browser/network-error-handler.ts +61 -0
- package/src/browser/partial-update.ts +376 -315
- package/src/browser/prefetch/cache.ts +314 -0
- package/src/browser/prefetch/fetch.ts +282 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +48 -0
- package/src/browser/prefetch/queue.ts +191 -0
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/rango-state.ts +152 -0
- package/src/browser/react/Link.tsx +255 -71
- package/src/browser/react/NavigationProvider.tsx +152 -24
- package/src/browser/react/context.ts +11 -0
- package/src/browser/react/filter-segment-order.ts +55 -0
- package/src/browser/react/index.ts +15 -12
- package/src/browser/react/location-state-shared.ts +95 -53
- package/src/browser/react/location-state.ts +60 -15
- package/src/browser/react/mount-context.ts +6 -1
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +29 -51
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +30 -120
- package/src/browser/react/use-link-status.ts +6 -5
- package/src/browser/react/use-navigation.ts +44 -65
- package/src/browser/react/use-params.ts +78 -0
- package/src/browser/react/use-pathname.ts +47 -0
- package/src/browser/react/use-reverse.ts +99 -0
- package/src/browser/react/use-router.ts +83 -0
- package/src/browser/react/use-search-params.ts +56 -0
- package/src/browser/react/use-segments.ts +85 -99
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +246 -64
- package/src/browser/scroll-restoration.ts +127 -52
- package/src/browser/segment-reconciler.ts +243 -0
- package/src/browser/segment-structure-assert.ts +16 -0
- package/src/browser/server-action-bridge.ts +510 -603
- package/src/browser/shallow.ts +6 -1
- package/src/browser/types.ts +158 -48
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +84 -23
- package/src/build/generate-route-types.ts +39 -828
- package/src/build/index.ts +4 -5
- package/src/build/route-trie.ts +85 -32
- package/src/build/route-types/ast-helpers.ts +25 -0
- package/src/build/route-types/ast-route-extraction.ts +98 -0
- package/src/build/route-types/codegen.ts +102 -0
- package/src/build/route-types/include-resolution.ts +418 -0
- package/src/build/route-types/param-extraction.ts +48 -0
- package/src/build/route-types/per-module-writer.ts +128 -0
- package/src/build/route-types/router-processing.ts +618 -0
- package/src/build/route-types/scan-filter.ts +85 -0
- package/src/build/runtime-discovery.ts +231 -0
- package/src/cache/background-task.ts +34 -0
- package/src/cache/cache-key-utils.ts +44 -0
- package/src/cache/cache-policy.ts +125 -0
- package/src/cache/cache-runtime.ts +342 -0
- package/src/cache/cache-scope.ts +167 -307
- package/src/cache/cf/cf-cache-store.ts +573 -21
- package/src/cache/cf/index.ts +13 -3
- package/src/cache/document-cache.ts +116 -77
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/handle-snapshot.ts +41 -0
- package/src/cache/index.ts +1 -15
- package/src/cache/memory-segment-store.ts +191 -13
- package/src/cache/profile-registry.ts +73 -0
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +256 -0
- package/src/cache/taint.ts +153 -0
- package/src/cache/types.ts +72 -122
- package/src/client.rsc.tsx +6 -1
- package/src/client.tsx +118 -302
- package/src/component-utils.ts +4 -4
- package/src/components/DefaultDocument.tsx +5 -1
- package/src/context-var.ts +156 -0
- package/src/debug.ts +19 -9
- package/src/errors.ts +77 -7
- package/src/handle.ts +55 -10
- package/src/handles/MetaTags.tsx +73 -20
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/handles/meta.ts +30 -13
- package/src/host/cookie-handler.ts +21 -15
- package/src/host/errors.ts +8 -8
- package/src/host/index.ts +4 -7
- package/src/host/pattern-matcher.ts +27 -27
- package/src/host/router.ts +61 -39
- package/src/host/testing.ts +8 -8
- package/src/host/types.ts +15 -7
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +65 -45
- package/src/index.rsc.ts +138 -21
- package/src/index.ts +206 -51
- package/src/internal-debug.ts +11 -0
- package/src/loader.rsc.ts +25 -143
- package/src/loader.ts +27 -10
- package/src/network-error-thrower.tsx +3 -1
- package/src/outlet-context.ts +1 -1
- package/src/outlet-provider.tsx +45 -0
- package/src/prerender/param-hash.ts +4 -2
- package/src/prerender/store.ts +159 -13
- package/src/prerender.ts +397 -29
- package/src/response-utils.ts +28 -0
- package/src/reverse.ts +231 -121
- package/src/root-error-boundary.tsx +41 -29
- package/src/route-content-wrapper.tsx +7 -4
- package/src/route-definition/dsl-helpers.ts +1134 -0
- package/src/route-definition/helper-factories.ts +200 -0
- package/src/route-definition/helpers-types.ts +483 -0
- package/src/route-definition/index.ts +55 -0
- package/src/route-definition/redirect.ts +101 -0
- package/src/route-definition/resolve-handler-use.ts +155 -0
- package/src/route-definition.ts +1 -1431
- package/src/route-map-builder.ts +162 -123
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +66 -9
- package/src/router/content-negotiation.ts +215 -0
- package/src/router/debug-manifest.ts +72 -0
- package/src/router/error-handling.ts +9 -9
- package/src/router/find-match.ts +160 -0
- package/src/router/handler-context.ts +418 -86
- package/src/router/intercept-resolution.ts +35 -20
- package/src/router/lazy-includes.ts +237 -0
- package/src/router/loader-resolution.ts +359 -128
- package/src/router/logging.ts +251 -0
- package/src/router/manifest.ts +98 -32
- package/src/router/match-api.ts +196 -261
- package/src/router/match-context.ts +4 -2
- package/src/router/match-handlers.ts +441 -0
- package/src/router/match-middleware/background-revalidation.ts +108 -93
- package/src/router/match-middleware/cache-lookup.ts +415 -86
- package/src/router/match-middleware/cache-store.ts +91 -29
- package/src/router/match-middleware/intercept-resolution.ts +48 -21
- package/src/router/match-middleware/segment-resolution.ts +73 -9
- package/src/router/match-pipelines.ts +10 -45
- package/src/router/match-result.ts +154 -35
- package/src/router/metrics.ts +240 -15
- package/src/router/middleware-cookies.ts +55 -0
- package/src/router/middleware-types.ts +209 -0
- package/src/router/middleware.ts +373 -371
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +292 -52
- package/src/router/prerender-match.ts +502 -0
- package/src/router/preview-match.ts +98 -0
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +152 -39
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +41 -21
- package/src/router/router-interfaces.ts +484 -0
- package/src/router/router-options.ts +618 -0
- package/src/router/router-registry.ts +24 -0
- package/src/router/segment-resolution/fresh.ts +756 -0
- package/src/router/segment-resolution/helpers.ts +268 -0
- package/src/router/segment-resolution/loader-cache.ts +199 -0
- package/src/router/segment-resolution/revalidation.ts +1407 -0
- package/src/router/segment-resolution/static-store.ts +67 -0
- package/src/router/segment-resolution.ts +21 -1315
- package/src/router/segment-wrappers.ts +291 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry-otel.ts +299 -0
- package/src/router/telemetry.ts +300 -0
- package/src/router/timeout.ts +148 -0
- package/src/router/trie-matching.ts +111 -39
- package/src/router/types.ts +17 -9
- package/src/router/url-params.ts +49 -0
- package/src/router.ts +642 -2011
- package/src/rsc/handler-context.ts +45 -0
- package/src/rsc/handler.ts +864 -1114
- package/src/rsc/helpers.ts +181 -19
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +229 -0
- package/src/rsc/manifest-init.ts +90 -0
- package/src/rsc/nonce.ts +14 -0
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +395 -0
- package/src/rsc/response-error.ts +37 -0
- package/src/rsc/response-route-handler.ts +360 -0
- package/src/rsc/rsc-rendering.ts +256 -0
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +360 -0
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +52 -11
- package/src/search-params.ts +230 -0
- package/src/segment-content-promise.ts +67 -0
- package/src/segment-loader-promise.ts +122 -0
- package/src/segment-system.tsx +187 -38
- package/src/server/context.ts +333 -59
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +37 -0
- package/src/server/handle-store.ts +113 -15
- package/src/server/loader-registry.ts +24 -64
- package/src/server/request-context.ts +603 -109
- package/src/server.ts +35 -155
- package/src/ssr/index.tsx +107 -30
- package/src/static-handler.ts +126 -0
- package/src/theme/ThemeProvider.tsx +21 -15
- package/src/theme/ThemeScript.tsx +5 -5
- package/src/theme/constants.ts +5 -2
- package/src/theme/index.ts +4 -14
- package/src/theme/theme-context.ts +4 -30
- package/src/theme/theme-script.ts +21 -18
- package/src/types/boundaries.ts +158 -0
- package/src/types/cache-types.ts +198 -0
- package/src/types/error-types.ts +192 -0
- package/src/types/global-namespace.ts +100 -0
- package/src/types/handler-context.ts +764 -0
- package/src/types/index.ts +88 -0
- package/src/types/loader-types.ts +209 -0
- package/src/types/request-scope.ts +126 -0
- package/src/types/route-config.ts +170 -0
- package/src/types/route-entry.ts +120 -0
- package/src/types/segments.ts +167 -0
- package/src/types.ts +1 -1757
- package/src/urls/include-helper.ts +207 -0
- package/src/urls/index.ts +53 -0
- package/src/urls/path-helper-types.ts +372 -0
- package/src/urls/path-helper.ts +364 -0
- package/src/urls/pattern-types.ts +107 -0
- package/src/urls/response-types.ts +108 -0
- package/src/urls/type-extraction.ts +372 -0
- package/src/urls/urls-function.ts +98 -0
- package/src/urls.ts +1 -1282
- package/src/use-loader.tsx +161 -81
- package/src/vite/debug.ts +184 -0
- package/src/vite/discovery/bundle-postprocess.ts +181 -0
- package/src/vite/discovery/discover-routers.ts +376 -0
- package/src/vite/discovery/gate-state.ts +171 -0
- package/src/vite/discovery/prerender-collection.ts +486 -0
- package/src/vite/discovery/route-types-writer.ts +258 -0
- package/src/vite/discovery/self-gen-tracking.ts +73 -0
- package/src/vite/discovery/state.ts +117 -0
- package/src/vite/discovery/virtual-module-codegen.ts +203 -0
- package/src/vite/index.ts +15 -2063
- package/src/vite/plugin-types.ts +103 -0
- package/src/vite/plugins/cjs-to-esm.ts +98 -0
- package/src/vite/plugins/client-ref-dedup.ts +131 -0
- package/src/vite/plugins/client-ref-hashing.ts +117 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
- package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
- package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
- package/src/vite/{expose-action-id.ts → plugins/expose-action-id.ts} +107 -64
- package/src/vite/plugins/expose-id-utils.ts +299 -0
- package/src/vite/plugins/expose-ids/export-analysis.ts +296 -0
- package/src/vite/plugins/expose-ids/handler-transform.ts +209 -0
- package/src/vite/plugins/expose-ids/loader-transform.ts +74 -0
- package/src/vite/plugins/expose-ids/router-transform.ts +127 -0
- package/src/vite/plugins/expose-ids/types.ts +45 -0
- package/src/vite/plugins/expose-internal-ids.ts +816 -0
- package/src/vite/plugins/performance-tracks.ts +96 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/use-cache-transform.ts +336 -0
- package/src/vite/plugins/version-injector.ts +109 -0
- package/src/vite/plugins/version-plugin.ts +266 -0
- package/src/vite/{virtual-entries.ts → plugins/virtual-entries.ts} +23 -14
- package/src/vite/plugins/virtual-stub-plugin.ts +29 -0
- package/src/vite/rango.ts +497 -0
- package/src/vite/router-discovery.ts +1423 -0
- package/src/vite/utils/ast-handler-extract.ts +517 -0
- package/src/vite/utils/banner.ts +36 -0
- package/src/vite/utils/bundle-analysis.ts +137 -0
- package/src/vite/utils/manifest-utils.ts +70 -0
- package/src/vite/utils/package-resolution.ts +161 -0
- package/src/vite/utils/prerender-utils.ts +222 -0
- package/src/vite/utils/shared-utils.ts +170 -0
- package/CLAUDE.md +0 -43
- package/src/browser/lru-cache.ts +0 -69
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/urls.gen.ts +0 -8
- package/src/vite/expose-handle-id.ts +0 -209
- package/src/vite/expose-loader-id.ts +0 -426
- package/src/vite/expose-location-state-id.ts +0 -177
- package/src/vite/expose-prerender-handler-id.ts +0 -429
- package/src/vite/package-resolution.ts +0 -125
- /package/src/vite/{version.d.ts → plugins/version.d.ts} +0 -0
package/src/rsc/helpers.ts
CHANGED
|
@@ -4,7 +4,52 @@
|
|
|
4
4
|
* Utility functions for RSC request handling.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
_getRequestContext,
|
|
9
|
+
getLocationState,
|
|
10
|
+
} from "../server/request-context.js";
|
|
11
|
+
import type { RequestContext } from "../server/request-context.js";
|
|
12
|
+
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
|
+
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Copy stub headers from the request context onto a target Headers instance:
|
|
17
|
+
* append Set-Cookie entries, set everything else only if absent. Header
|
|
18
|
+
* mutation failures are swallowed so the same logic works against Response
|
|
19
|
+
* headers that may be immutable (e.g. Cloudflare protocol-switch responses).
|
|
20
|
+
*/
|
|
21
|
+
function applyStubHeaders(target: Headers, stub: Headers): void {
|
|
22
|
+
stub.forEach((value, name) => {
|
|
23
|
+
try {
|
|
24
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
25
|
+
target.append(name, value);
|
|
26
|
+
} else if (!target.has(name)) {
|
|
27
|
+
target.set(name, value);
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// Headers immutable — skip.
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Drain ctx._onResponseCallbacks onto a response. Swapping the array before
|
|
37
|
+
* iteration prevents re-entrant registrations from double-firing and matches
|
|
38
|
+
* the contract that each callback runs at most once per request.
|
|
39
|
+
*/
|
|
40
|
+
function drainOnResponseCallbacks(
|
|
41
|
+
ctx: RequestContext,
|
|
42
|
+
response: Response,
|
|
43
|
+
): Response {
|
|
44
|
+
const callbacks = ctx._onResponseCallbacks;
|
|
45
|
+
if (callbacks.length === 0) return response;
|
|
46
|
+
ctx._onResponseCallbacks = [];
|
|
47
|
+
let result = response;
|
|
48
|
+
for (const callback of callbacks) {
|
|
49
|
+
result = callback(result) ?? result;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
8
53
|
|
|
9
54
|
/**
|
|
10
55
|
* Check if a request body has content to decode
|
|
@@ -27,38 +72,155 @@ export function hasBodyContent(body: FormData | string): boolean {
|
|
|
27
72
|
*/
|
|
28
73
|
export function createResponseWithMergedHeaders(
|
|
29
74
|
body: BodyInit | null,
|
|
30
|
-
init: ResponseInit
|
|
75
|
+
init: ResponseInit,
|
|
31
76
|
): Response {
|
|
32
|
-
const ctx =
|
|
77
|
+
const ctx = _getRequestContext();
|
|
33
78
|
if (!ctx) {
|
|
34
79
|
return new Response(body, init);
|
|
35
80
|
}
|
|
36
81
|
|
|
37
|
-
//
|
|
82
|
+
// Delete Set-Cookie from the stub after consuming so downstream merge
|
|
83
|
+
// points (e.g. executeMiddleware) don't duplicate them.
|
|
38
84
|
const mergedHeaders = new Headers(init.headers);
|
|
39
|
-
ctx.res.headers
|
|
40
|
-
|
|
41
|
-
mergedHeaders.append(name, value);
|
|
42
|
-
} else if (!mergedHeaders.has(name)) {
|
|
43
|
-
// Only set if not already present in init.headers
|
|
44
|
-
mergedHeaders.set(name, value);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
85
|
+
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
86
|
+
ctx.res.headers.delete("set-cookie");
|
|
47
87
|
|
|
48
|
-
//
|
|
49
|
-
//
|
|
88
|
+
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
89
|
+
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
50
90
|
const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
|
|
51
91
|
|
|
52
|
-
|
|
92
|
+
const response = new Response(body, {
|
|
53
93
|
...init,
|
|
54
94
|
status,
|
|
55
95
|
headers: mergedHeaders,
|
|
56
96
|
});
|
|
57
97
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
98
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Create a 204 response with X-RSC-Redirect header for stateless redirects.
|
|
103
|
+
* Used during partial/action requests where fetch would auto-follow a raw
|
|
104
|
+
* 3xx to a URL that renders full HTML instead of Flight data. The 204 status
|
|
105
|
+
* prevents auto-follow; the client reads the header and re-navigates via
|
|
106
|
+
* the router.
|
|
107
|
+
*/
|
|
108
|
+
export function createSimpleRedirectResponse(redirectUrl: string): Response {
|
|
109
|
+
return createResponseWithMergedHeaders(null, {
|
|
110
|
+
status: 204,
|
|
111
|
+
headers: { "X-RSC-Redirect": redirectUrl },
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Carry over headers from a source redirect Response to a wrapper Response.
|
|
117
|
+
* Skips Location and X-RSC-Redirect (intentionally replaced by the wrapper)
|
|
118
|
+
* and appends Set-Cookie to avoid clobbering multiple cookie headers.
|
|
119
|
+
*/
|
|
120
|
+
export function carryOverRedirectHeaders(
|
|
121
|
+
source: Response,
|
|
122
|
+
target: Response,
|
|
123
|
+
): void {
|
|
124
|
+
source.headers.forEach((value, name) => {
|
|
125
|
+
const lower = name.toLowerCase();
|
|
126
|
+
if (lower === "location" || lower === "x-rsc-redirect") return;
|
|
127
|
+
if (lower === "set-cookie") {
|
|
128
|
+
target.headers.append(name, value);
|
|
129
|
+
} else if (!target.headers.has(name)) {
|
|
130
|
+
target.headers.set(name, value);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* If a response is a 3xx redirect during a partial (client-side) request,
|
|
137
|
+
* intercept it and return a Flight-compatible redirect instead.
|
|
138
|
+
* fetch() auto-follows 3xx which would hit a URL that renders full HTML
|
|
139
|
+
* the client can't parse. Returns null if the response is not a redirect.
|
|
140
|
+
*/
|
|
141
|
+
export function interceptRedirectForPartial(
|
|
142
|
+
response: Response,
|
|
143
|
+
createRedirectFlightResponse: (
|
|
144
|
+
redirectUrl: string,
|
|
145
|
+
locationState?: Record<string, unknown>,
|
|
146
|
+
) => Response,
|
|
147
|
+
): Response | null {
|
|
148
|
+
const redirectUrl = response.headers.get("Location");
|
|
149
|
+
if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
const locationState = getLocationState();
|
|
153
|
+
let intercepted: Response;
|
|
154
|
+
if (locationState) {
|
|
155
|
+
intercepted = createRedirectFlightResponse(
|
|
156
|
+
redirectUrl,
|
|
157
|
+
resolveLocationStateEntries(locationState),
|
|
158
|
+
);
|
|
159
|
+
} else {
|
|
160
|
+
intercepted = createSimpleRedirectResponse(redirectUrl);
|
|
61
161
|
}
|
|
62
162
|
|
|
63
|
-
|
|
163
|
+
carryOverRedirectHeaders(response, intercepted);
|
|
164
|
+
|
|
165
|
+
return intercepted;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Only cache successful responses. Non-200 statuses (errors, redirects) are
|
|
170
|
+
* not cached -- notFound() produces 500 in response routes, and explicit
|
|
171
|
+
* non-200 Responses are rare enough that caching them would be surprising.
|
|
172
|
+
*/
|
|
173
|
+
export function isCacheableStatus(status: number): boolean {
|
|
174
|
+
return status === 200;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Convert route-level middleware entries to the format expected by
|
|
179
|
+
* executeMiddleware. Route middleware from previewMatch carries just
|
|
180
|
+
* { handler, params }; this wraps them in the full MiddlewareEntry shape.
|
|
181
|
+
*/
|
|
182
|
+
export function buildRouteMiddlewareEntries<TEnv>(
|
|
183
|
+
routeMiddleware: Array<{
|
|
184
|
+
handler: MiddlewareFn;
|
|
185
|
+
params: Record<string, string>;
|
|
186
|
+
}>,
|
|
187
|
+
): Array<{ entry: MiddlewareEntry<TEnv>; params: Record<string, string> }> {
|
|
188
|
+
return routeMiddleware.map((mw) => ({
|
|
189
|
+
entry: {
|
|
190
|
+
pattern: null,
|
|
191
|
+
regex: null,
|
|
192
|
+
paramNames: [],
|
|
193
|
+
handler: mw.handler,
|
|
194
|
+
mountPrefix: null,
|
|
195
|
+
} as MiddlewareEntry<TEnv>,
|
|
196
|
+
params: mw.params,
|
|
197
|
+
}));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Merge stub headers from the request context onto an existing Response in
|
|
202
|
+
* place, then drain onResponse callbacks. Used when a Response cannot flow
|
|
203
|
+
* through `new Response()` — status 101 is outside the constructor's
|
|
204
|
+
* 200-599 range, and the Cloudflare-specific `webSocket` property would be
|
|
205
|
+
* lost on reconstruction.
|
|
206
|
+
*/
|
|
207
|
+
export function mergeStubHeadersAndFinalize(response: Response): Response {
|
|
208
|
+
const ctx = _getRequestContext();
|
|
209
|
+
if (!ctx) return response;
|
|
210
|
+
|
|
211
|
+
applyStubHeaders(response.headers, ctx.res.headers);
|
|
212
|
+
ctx.res.headers.delete("set-cookie");
|
|
213
|
+
|
|
214
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Run onResponse callbacks on an existing Response. Used by code paths that
|
|
219
|
+
* bypass createResponseWithMergedHeaders (e.g. middleware short-circuits)
|
|
220
|
+
* but still need ctx.onResponse() callbacks to fire.
|
|
221
|
+
*/
|
|
222
|
+
export function finalizeResponse(response: Response): Response {
|
|
223
|
+
const ctx = _getRequestContext();
|
|
224
|
+
if (!ctx) return response;
|
|
225
|
+
return drainOnResponseCallbacks(ctx, response);
|
|
64
226
|
}
|
package/src/rsc/index.ts
CHANGED
|
@@ -29,28 +29,8 @@ export type {
|
|
|
29
29
|
NonceProvider,
|
|
30
30
|
} from "./types.js";
|
|
31
31
|
|
|
32
|
-
// Re-export HandleStore types for consumers who need custom handling
|
|
33
|
-
export {
|
|
34
|
-
createHandleStore,
|
|
35
|
-
type HandleStore,
|
|
36
|
-
type HandleData,
|
|
37
|
-
} from "../server/handle-store.js";
|
|
38
|
-
|
|
39
32
|
// Re-export request context utilities for server-side access to env/request/params
|
|
40
33
|
export {
|
|
41
34
|
getRequestContext,
|
|
42
35
|
requireRequestContext,
|
|
43
|
-
setRequestContextParams,
|
|
44
36
|
} from "../server/request-context.js";
|
|
45
|
-
|
|
46
|
-
// Re-export cache store types and implementations
|
|
47
|
-
export type {
|
|
48
|
-
SegmentCacheStore,
|
|
49
|
-
CachedEntryData,
|
|
50
|
-
CachedEntryResult,
|
|
51
|
-
SegmentCacheProvider,
|
|
52
|
-
SegmentHandleData,
|
|
53
|
-
} from "../cache/types.js";
|
|
54
|
-
|
|
55
|
-
export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
|
|
56
|
-
export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loader Fetch Handler
|
|
3
|
+
*
|
|
4
|
+
* Handles load() requests (GET, POST, PUT, PATCH, DELETE) from the client.
|
|
5
|
+
* All loader data fetching and mutations go through this endpoint.
|
|
6
|
+
*
|
|
7
|
+
* Route params (e.g. slug from /blog/:slug) come from previewMatch() in the
|
|
8
|
+
* outer coreRequestHandler, threaded through coreRequestHandlerInner as
|
|
9
|
+
* routeParams. This is necessary because handleLoaderFetch doesn't do its
|
|
10
|
+
* own route matching -- the URL is the page's pathname, and previewMatch
|
|
11
|
+
* has already extracted params from it.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { getLoaderLazy } from "../server/loader-registry.js";
|
|
15
|
+
import { executeLoaderMiddleware } from "../router/middleware.js";
|
|
16
|
+
import { requireRequestContext } from "../server/request-context.js";
|
|
17
|
+
import {
|
|
18
|
+
createReverseFunction,
|
|
19
|
+
stripInternalParams,
|
|
20
|
+
} from "../router/handler-context.js";
|
|
21
|
+
import {
|
|
22
|
+
getGlobalRouteMap,
|
|
23
|
+
getSearchSchema,
|
|
24
|
+
isRouteRootScoped,
|
|
25
|
+
} from "../route-map-builder.js";
|
|
26
|
+
import { parseSearchParams } from "../search-params.js";
|
|
27
|
+
import {
|
|
28
|
+
createResponseWithMergedHeaders,
|
|
29
|
+
finalizeResponse,
|
|
30
|
+
} from "./helpers.js";
|
|
31
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
32
|
+
|
|
33
|
+
export async function handleLoaderFetch<TEnv>(
|
|
34
|
+
ctx: HandlerContext<TEnv>,
|
|
35
|
+
request: Request,
|
|
36
|
+
env: TEnv,
|
|
37
|
+
url: URL,
|
|
38
|
+
variables: Record<string, any>,
|
|
39
|
+
routeParams?: Record<string, string>,
|
|
40
|
+
): Promise<Response> {
|
|
41
|
+
const loaderId = url.searchParams.get("_rsc_loader");
|
|
42
|
+
|
|
43
|
+
if (!loaderId) {
|
|
44
|
+
return createResponseWithMergedHeaders("Missing _rsc_loader parameter", {
|
|
45
|
+
status: 400,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Look up loader lazily
|
|
50
|
+
const registeredLoader = await getLoaderLazy(loaderId);
|
|
51
|
+
if (!registeredLoader) {
|
|
52
|
+
return createResponseWithMergedHeaders(
|
|
53
|
+
`Loader "${loaderId}" not found in registry`,
|
|
54
|
+
{ status: 404 },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Non-fetchable loaders are registered for SSR ctx.use() only.
|
|
59
|
+
// They must not be callable through the standalone _rsc_loader endpoint.
|
|
60
|
+
if (!registeredLoader.fetchable) {
|
|
61
|
+
return createResponseWithMergedHeaders(
|
|
62
|
+
`Loader "${loaderId}" is not fetchable`,
|
|
63
|
+
{ status: 403 },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse params, body, and formData based on request method and content type
|
|
68
|
+
let loaderParams: Record<string, string> = {};
|
|
69
|
+
let loaderBody: unknown = undefined;
|
|
70
|
+
let loaderFormData: FormData | undefined;
|
|
71
|
+
const isBodyMethod = request.method !== "GET" && request.method !== "HEAD";
|
|
72
|
+
|
|
73
|
+
if (isBodyMethod) {
|
|
74
|
+
try {
|
|
75
|
+
const contentType = request.headers.get("content-type") || "";
|
|
76
|
+
if (contentType.includes("multipart/form-data")) {
|
|
77
|
+
// FormData body — sent by load() when body is a FormData instance.
|
|
78
|
+
// Preserves File objects and binary data.
|
|
79
|
+
loaderFormData = await request.formData();
|
|
80
|
+
// Extract params if provided via the special field
|
|
81
|
+
const paramsField = loaderFormData.get("_rsc_loader_params");
|
|
82
|
+
if (typeof paramsField === "string") {
|
|
83
|
+
loaderParams = JSON.parse(paramsField);
|
|
84
|
+
loaderFormData.delete("_rsc_loader_params");
|
|
85
|
+
}
|
|
86
|
+
} else if (contentType.includes("application/json")) {
|
|
87
|
+
const jsonBody = (await request.json()) as {
|
|
88
|
+
params?: Record<string, string>;
|
|
89
|
+
body?: unknown;
|
|
90
|
+
};
|
|
91
|
+
loaderParams = jsonBody.params ?? {};
|
|
92
|
+
loaderBody = jsonBody.body;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
return createResponseWithMergedHeaders("Invalid request body", {
|
|
96
|
+
status: 400,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
const loaderParamsJson = url.searchParams.get("_rsc_loader_params");
|
|
101
|
+
if (loaderParamsJson) {
|
|
102
|
+
try {
|
|
103
|
+
loaderParams = JSON.parse(loaderParamsJson);
|
|
104
|
+
} catch {
|
|
105
|
+
return createResponseWithMergedHeaders(
|
|
106
|
+
"Invalid _rsc_loader_params JSON",
|
|
107
|
+
{ status: 400 },
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Execute the loader with middleware.
|
|
114
|
+
// finalizeResponse drains onResponse callbacks that middleware short-circuits
|
|
115
|
+
// may leave behind (executeLoaderMiddleware does not finalize them itself).
|
|
116
|
+
try {
|
|
117
|
+
const { fn, middleware } = registeredLoader;
|
|
118
|
+
|
|
119
|
+
return finalizeResponse(
|
|
120
|
+
await executeLoaderMiddleware(
|
|
121
|
+
middleware,
|
|
122
|
+
request,
|
|
123
|
+
env,
|
|
124
|
+
loaderParams,
|
|
125
|
+
variables,
|
|
126
|
+
async () => {
|
|
127
|
+
const reqCtx = requireRequestContext();
|
|
128
|
+
// Merge route params (from previewMatch) with explicit loader params.
|
|
129
|
+
// Explicit params take precedence over route-matched params.
|
|
130
|
+
const resolvedRouteParams = routeParams ?? {};
|
|
131
|
+
const mergedParams = {
|
|
132
|
+
...resolvedRouteParams,
|
|
133
|
+
...loaderParams,
|
|
134
|
+
};
|
|
135
|
+
// Strip _rsc_* transport params so loaders see the same
|
|
136
|
+
// url/searchParams as during SSR/navigation.
|
|
137
|
+
const cleanUrl = stripInternalParams(url);
|
|
138
|
+
const cleanSearchParams = cleanUrl.searchParams;
|
|
139
|
+
const searchSchema = reqCtx._routeName
|
|
140
|
+
? getSearchSchema(reqCtx._routeName)
|
|
141
|
+
: undefined;
|
|
142
|
+
const loaderCtx: any = {
|
|
143
|
+
...reqCtx,
|
|
144
|
+
url: cleanUrl,
|
|
145
|
+
pathname: cleanUrl.pathname,
|
|
146
|
+
searchParams: cleanSearchParams,
|
|
147
|
+
search: searchSchema
|
|
148
|
+
? parseSearchParams(cleanSearchParams, searchSchema)
|
|
149
|
+
: {},
|
|
150
|
+
params: mergedParams,
|
|
151
|
+
routeParams: resolvedRouteParams,
|
|
152
|
+
body: loaderBody,
|
|
153
|
+
method: request.method,
|
|
154
|
+
reverse: createReverseFunction(
|
|
155
|
+
getGlobalRouteMap(),
|
|
156
|
+
reqCtx._routeName,
|
|
157
|
+
mergedParams,
|
|
158
|
+
reqCtx._routeName
|
|
159
|
+
? isRouteRootScoped(reqCtx._routeName)
|
|
160
|
+
: undefined,
|
|
161
|
+
),
|
|
162
|
+
...(loaderFormData ? { formData: loaderFormData } : {}),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const result = await fn(loaderCtx);
|
|
166
|
+
|
|
167
|
+
interface LoaderPayload {
|
|
168
|
+
loaderResult: unknown;
|
|
169
|
+
}
|
|
170
|
+
const loaderPayload: LoaderPayload = { loaderResult: result };
|
|
171
|
+
const rscStream = ctx.renderToReadableStream<LoaderPayload>(
|
|
172
|
+
loaderPayload,
|
|
173
|
+
{
|
|
174
|
+
onError: (error: unknown) => {
|
|
175
|
+
ctx.callOnError(error, "rendering", {
|
|
176
|
+
request,
|
|
177
|
+
url,
|
|
178
|
+
env,
|
|
179
|
+
loaderName: loaderId,
|
|
180
|
+
});
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
186
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
createReverseFunction(ctx.getRequiredRouteMap()),
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
194
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
195
|
+
|
|
196
|
+
console.error("[RSC] Loader error:", error);
|
|
197
|
+
|
|
198
|
+
ctx.callOnError(error, "loader", {
|
|
199
|
+
request,
|
|
200
|
+
url,
|
|
201
|
+
env,
|
|
202
|
+
loaderName: loaderId,
|
|
203
|
+
handledByBoundary: false,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const errorPayload = {
|
|
207
|
+
loaderResult: null,
|
|
208
|
+
loaderError: {
|
|
209
|
+
message: isDev ? err.message : "An error occurred",
|
|
210
|
+
name: err.name,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
const rscStream = ctx.renderToReadableStream(errorPayload, {
|
|
214
|
+
onError: (error: unknown) => {
|
|
215
|
+
ctx.callOnError(error, "rendering", {
|
|
216
|
+
request,
|
|
217
|
+
url,
|
|
218
|
+
env,
|
|
219
|
+
loaderName: loaderId,
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return createResponseWithMergedHeaders(rscStream, {
|
|
225
|
+
status: 500,
|
|
226
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Router Manifest Initialization
|
|
3
|
+
*
|
|
4
|
+
* Builds a fresh route trie from router.urlpatterns for dev/HMR scenarios
|
|
5
|
+
* where the manifest exists but the trie needs rebuilding.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
getGlobalRouteMap,
|
|
10
|
+
hasCachedManifest,
|
|
11
|
+
setCachedManifest,
|
|
12
|
+
getRouteTrie,
|
|
13
|
+
setRouteTrie,
|
|
14
|
+
setRouterManifest,
|
|
15
|
+
setRouterTrie,
|
|
16
|
+
} from "../route-map-builder.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a fresh route trie from router.urlpatterns and store it in the
|
|
20
|
+
* per-router cache. Also sets the per-router manifest and merges into
|
|
21
|
+
* the global manifest for reverse()/href().
|
|
22
|
+
*
|
|
23
|
+
* Called when manifest data may exist but the per-router trie is missing,
|
|
24
|
+
* which happens in dev mode after HMR: the virtual module sets the manifest
|
|
25
|
+
* from fresh gen files but skips the trie (which would be stale from initial
|
|
26
|
+
* discovery). The trie is essential for correct wildcard priority -- without
|
|
27
|
+
* it, the regex fallback matches catch-all patterns before specific routes.
|
|
28
|
+
*/
|
|
29
|
+
export async function buildRouterTrieFromUrlpatterns(
|
|
30
|
+
router: any,
|
|
31
|
+
): Promise<void> {
|
|
32
|
+
const { generateManifestFull } =
|
|
33
|
+
await import("../build/generate-manifest.js");
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
39
|
+
if (
|
|
40
|
+
generated._routeAncestry &&
|
|
41
|
+
Object.keys(generated._routeAncestry).length > 0
|
|
42
|
+
) {
|
|
43
|
+
const { buildRouteTrie } = await import("../build/route-trie.js");
|
|
44
|
+
// Map each route to its include() staticPrefix so the trie
|
|
45
|
+
// returns the correct sp for lazy entry lookup in findMatch.
|
|
46
|
+
const routeToStaticPrefix: Record<string, string> = {};
|
|
47
|
+
for (const name of Object.keys(generated.routeManifest)) {
|
|
48
|
+
routeToStaticPrefix[name] = "";
|
|
49
|
+
}
|
|
50
|
+
// Override with prefix from include() entries so the trie
|
|
51
|
+
// returns the correct sp for lazy entry lookup in findMatch.
|
|
52
|
+
// Walk recursively to include routes in nested includes.
|
|
53
|
+
if (generated.prefixTree) {
|
|
54
|
+
const visitPrefixNode = (node: any): void => {
|
|
55
|
+
const sp = node.staticPrefix || "";
|
|
56
|
+
for (const route of node.routes || []) {
|
|
57
|
+
routeToStaticPrefix[route] = sp;
|
|
58
|
+
}
|
|
59
|
+
for (const child of Object.values(node.children || {})) {
|
|
60
|
+
visitPrefixNode(child);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
for (const node of Object.values(generated.prefixTree)) {
|
|
64
|
+
visitPrefixNode(node);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const trie = buildRouteTrie(
|
|
68
|
+
generated.routeManifest,
|
|
69
|
+
generated._routeAncestry,
|
|
70
|
+
routeToStaticPrefix,
|
|
71
|
+
generated.routeTrailingSlash,
|
|
72
|
+
generated.prerenderRoutes
|
|
73
|
+
? new Set(generated.prerenderRoutes)
|
|
74
|
+
: undefined,
|
|
75
|
+
generated.passthroughRoutes
|
|
76
|
+
? new Set(generated.passthroughRoutes)
|
|
77
|
+
: undefined,
|
|
78
|
+
generated.responseTypeRoutes,
|
|
79
|
+
);
|
|
80
|
+
setRouterTrie(router.id, trie);
|
|
81
|
+
// Set global trie only if not already set by another router
|
|
82
|
+
if (!getRouteTrie()) {
|
|
83
|
+
setRouteTrie(trie);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
setRouterManifest(router.id, generated.routeManifest);
|
|
87
|
+
// Merge into global manifest (needed for reverse/href across routers)
|
|
88
|
+
const existing = hasCachedManifest() ? getGlobalRouteMap() : {};
|
|
89
|
+
setCachedManifest({ ...existing, ...generated.routeManifest });
|
|
90
|
+
}
|
package/src/rsc/nonce.ts
CHANGED
|
@@ -2,6 +2,20 @@
|
|
|
2
2
|
* Nonce generation for Content Security Policy (CSP)
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { ContextVar } from "../context-var.js";
|
|
6
|
+
import { createVar } from "../context-var.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Typed ContextVar token for CSP nonce.
|
|
10
|
+
*
|
|
11
|
+
* Use this to access the nonce in middleware or handlers:
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { nonce } from "@rangojs/router";
|
|
14
|
+
* const value = ctx.get(nonce); // string | undefined
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const nonce: ContextVar<string> = createVar<string>();
|
|
18
|
+
|
|
5
19
|
/**
|
|
6
20
|
* Generate a cryptographic nonce for CSP.
|
|
7
21
|
* Returns a 16-byte random value encoded as base64.
|