@rangojs/router 0.0.0-experimental.19 → 0.0.0-experimental.1fa245e2
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/{CLAUDE.md → AGENTS.md} +4 -0
- package/README.md +122 -30
- package/dist/bin/rango.js +245 -63
- package/dist/vite/index.js +859 -418
- package/package.json +3 -3
- package/skills/breadcrumbs/SKILL.md +250 -0
- package/skills/cache-guide/SKILL.md +32 -0
- package/skills/caching/SKILL.md +49 -8
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +33 -31
- package/skills/host-router/SKILL.md +218 -0
- package/skills/links/SKILL.md +3 -1
- package/skills/loader/SKILL.md +72 -22
- package/skills/middleware/SKILL.md +2 -0
- package/skills/parallel/SKILL.md +126 -0
- package/skills/prerender/SKILL.md +112 -70
- package/skills/rango/SKILL.md +0 -1
- package/skills/route/SKILL.md +34 -4
- package/skills/router-setup/SKILL.md +95 -5
- package/skills/typesafety/SKILL.md +35 -23
- package/src/__internal.ts +92 -0
- package/src/bin/rango.ts +18 -0
- package/src/browser/app-version.ts +14 -0
- package/src/browser/event-controller.ts +5 -0
- package/src/browser/link-interceptor.ts +4 -0
- package/src/browser/navigation-bridge.ts +114 -18
- package/src/browser/navigation-client.ts +126 -44
- package/src/browser/navigation-store.ts +43 -8
- package/src/browser/navigation-transaction.ts +11 -9
- package/src/browser/partial-update.ts +80 -15
- package/src/browser/prefetch/cache.ts +166 -27
- package/src/browser/prefetch/fetch.ts +52 -39
- package/src/browser/prefetch/policy.ts +6 -0
- package/src/browser/prefetch/queue.ts +92 -20
- package/src/browser/prefetch/resource-ready.ts +77 -0
- package/src/browser/react/Link.tsx +70 -14
- package/src/browser/react/NavigationProvider.tsx +40 -4
- package/src/browser/react/context.ts +7 -2
- package/src/browser/react/use-handle.ts +9 -58
- package/src/browser/react/use-router.ts +21 -8
- package/src/browser/rsc-router.tsx +143 -59
- package/src/browser/scroll-restoration.ts +41 -42
- package/src/browser/segment-reconciler.ts +6 -1
- package/src/browser/server-action-bridge.ts +454 -436
- package/src/browser/types.ts +60 -5
- package/src/build/generate-manifest.ts +6 -6
- package/src/build/generate-route-types.ts +5 -0
- package/src/build/route-trie.ts +19 -3
- package/src/build/route-types/include-resolution.ts +8 -1
- package/src/build/route-types/router-processing.ts +346 -87
- package/src/build/route-types/scan-filter.ts +8 -1
- package/src/cache/cache-runtime.ts +15 -11
- package/src/cache/cache-scope.ts +48 -7
- package/src/cache/cf/cf-cache-store.ts +453 -11
- package/src/cache/cf/index.ts +5 -1
- package/src/cache/document-cache.ts +17 -7
- package/src/cache/index.ts +1 -0
- package/src/cache/taint.ts +55 -0
- package/src/client.rsc.tsx +2 -1
- package/src/client.tsx +3 -102
- package/src/context-var.ts +72 -2
- package/src/debug.ts +2 -2
- package/src/handle.ts +40 -0
- package/src/handles/breadcrumbs.ts +66 -0
- package/src/handles/index.ts +1 -0
- package/src/host/index.ts +0 -3
- package/src/index.rsc.ts +8 -37
- package/src/index.ts +40 -66
- package/src/prerender/store.ts +57 -15
- package/src/prerender.ts +138 -77
- package/src/reverse.ts +22 -1
- package/src/route-definition/dsl-helpers.ts +73 -25
- package/src/route-definition/helpers-types.ts +10 -6
- package/src/route-definition/index.ts +3 -3
- package/src/route-definition/redirect.ts +11 -3
- package/src/route-definition/resolve-handler-use.ts +149 -0
- package/src/route-map-builder.ts +7 -1
- package/src/route-types.ts +11 -0
- package/src/router/content-negotiation.ts +100 -1
- package/src/router/find-match.ts +4 -2
- package/src/router/handler-context.ts +108 -25
- package/src/router/intercept-resolution.ts +11 -4
- package/src/router/lazy-includes.ts +4 -1
- package/src/router/loader-resolution.ts +123 -11
- package/src/router/logging.ts +5 -2
- package/src/router/manifest.ts +9 -3
- package/src/router/match-api.ts +125 -190
- package/src/router/match-middleware/background-revalidation.ts +30 -2
- package/src/router/match-middleware/cache-lookup.ts +88 -16
- package/src/router/match-middleware/cache-store.ts +53 -10
- package/src/router/match-middleware/intercept-resolution.ts +9 -7
- package/src/router/match-middleware/segment-resolution.ts +61 -5
- package/src/router/match-result.ts +22 -15
- package/src/router/metrics.ts +238 -13
- package/src/router/middleware-types.ts +53 -12
- package/src/router/middleware.ts +172 -85
- package/src/router/navigation-snapshot.ts +182 -0
- package/src/router/pattern-matching.ts +20 -5
- package/src/router/prerender-match.ts +114 -10
- package/src/router/preview-match.ts +30 -102
- package/src/router/request-classification.ts +310 -0
- package/src/router/revalidation.ts +27 -7
- package/src/router/route-snapshot.ts +245 -0
- package/src/router/router-context.ts +6 -1
- package/src/router/router-interfaces.ts +50 -5
- package/src/router/router-options.ts +50 -19
- package/src/router/segment-resolution/fresh.ts +200 -19
- package/src/router/segment-resolution/helpers.ts +30 -25
- package/src/router/segment-resolution/loader-cache.ts +1 -0
- package/src/router/segment-resolution/revalidation.ts +429 -301
- package/src/router/segment-wrappers.ts +2 -0
- package/src/router/trie-matching.ts +20 -2
- package/src/router/types.ts +1 -0
- package/src/router.ts +88 -15
- package/src/rsc/handler.ts +546 -359
- package/src/rsc/index.ts +0 -20
- package/src/rsc/manifest-init.ts +5 -1
- package/src/rsc/progressive-enhancement.ts +25 -8
- package/src/rsc/rsc-rendering.ts +35 -43
- package/src/rsc/server-action.ts +16 -10
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +10 -1
- package/src/search-params.ts +16 -13
- package/src/segment-system.tsx +140 -4
- package/src/server/context.ts +148 -16
- package/src/server/loader-registry.ts +9 -8
- package/src/server/request-context.ts +182 -34
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +4 -0
- package/src/static-handler.ts +18 -6
- package/src/theme/index.ts +4 -13
- package/src/types/cache-types.ts +4 -4
- package/src/types/handler-context.ts +149 -49
- package/src/types/loader-types.ts +36 -9
- package/src/types/route-config.ts +17 -8
- package/src/types/route-entry.ts +8 -1
- package/src/types/segments.ts +2 -5
- package/src/urls/path-helper-types.ts +9 -2
- package/src/urls/path-helper.ts +48 -13
- package/src/urls/pattern-types.ts +12 -0
- package/src/urls/response-types.ts +16 -6
- package/src/use-loader.tsx +73 -4
- package/src/vite/discovery/bundle-postprocess.ts +61 -89
- package/src/vite/discovery/discover-routers.ts +23 -5
- package/src/vite/discovery/prerender-collection.ts +48 -15
- package/src/vite/discovery/state.ts +17 -13
- package/src/vite/index.ts +8 -3
- package/src/vite/plugin-types.ts +51 -79
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/expose-action-id.ts +1 -3
- package/src/vite/plugins/performance-tracks.ts +88 -0
- package/src/vite/plugins/refresh-cmd.ts +127 -0
- package/src/vite/plugins/version-plugin.ts +13 -1
- package/src/vite/rango.ts +174 -211
- package/src/vite/router-discovery.ts +169 -42
- package/src/vite/utils/banner.ts +3 -3
- package/src/vite/utils/prerender-utils.ts +78 -0
- package/src/vite/utils/shared-utils.ts +3 -2
- package/skills/testing/SKILL.md +0 -226
- package/src/route-definition/route-function.ts +0 -119
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";
|
package/src/rsc/manifest-init.ts
CHANGED
|
@@ -31,7 +31,11 @@ export async function buildRouterTrieFromUrlpatterns(
|
|
|
31
31
|
): Promise<void> {
|
|
32
32
|
const { generateManifestFull } =
|
|
33
33
|
await import("../build/generate-manifest.js");
|
|
34
|
-
const generated = generateManifestFull(
|
|
34
|
+
const generated = generateManifestFull(
|
|
35
|
+
router.urlpatterns,
|
|
36
|
+
undefined,
|
|
37
|
+
router.basename ? { urlPrefix: router.basename } : undefined,
|
|
38
|
+
);
|
|
35
39
|
if (
|
|
36
40
|
generated._routeAncestry &&
|
|
37
41
|
Object.keys(generated._routeAncestry).length > 0
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
requireRequestContext,
|
|
11
11
|
setRequestContextParams,
|
|
12
12
|
} from "../server/request-context.js";
|
|
13
|
+
import { getSSRSetup } from "./ssr-setup.js";
|
|
13
14
|
import type { MiddlewareFn } from "../router/middleware.js";
|
|
14
15
|
import { executeMiddleware } from "../router/middleware.js";
|
|
15
16
|
import type { RscPayload, ReactFormState } from "./types.js";
|
|
@@ -242,6 +243,8 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
242
243
|
const payload: RscPayload = {
|
|
243
244
|
metadata: {
|
|
244
245
|
pathname: url.pathname,
|
|
246
|
+
routerId: ctx.router.id,
|
|
247
|
+
basename: ctx.router.basename,
|
|
245
248
|
segments: match.segments,
|
|
246
249
|
matched: match.matched,
|
|
247
250
|
diff: match.diff,
|
|
@@ -257,10 +260,16 @@ export async function handleProgressiveEnhancement<TEnv>(
|
|
|
257
260
|
};
|
|
258
261
|
|
|
259
262
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
]
|
|
263
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
264
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
265
|
+
// without falling back to a fresh startSSRSetup.
|
|
266
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
267
|
+
ctx,
|
|
268
|
+
request,
|
|
269
|
+
env,
|
|
270
|
+
url,
|
|
271
|
+
undefined,
|
|
272
|
+
);
|
|
264
273
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
265
274
|
formState: reactFormState,
|
|
266
275
|
nonce,
|
|
@@ -335,6 +344,8 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
335
344
|
const payload: RscPayload = {
|
|
336
345
|
metadata: {
|
|
337
346
|
pathname: url.pathname,
|
|
347
|
+
routerId: ctx.router.id,
|
|
348
|
+
basename: ctx.router.basename,
|
|
338
349
|
segments: errorResult.segments,
|
|
339
350
|
matched: errorResult.matched,
|
|
340
351
|
diff: errorResult.diff,
|
|
@@ -350,10 +361,16 @@ async function renderPeErrorBoundary<TEnv>(
|
|
|
350
361
|
};
|
|
351
362
|
|
|
352
363
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
]
|
|
364
|
+
// metricsStore=undefined is safe: the handler already stashed the early
|
|
365
|
+
// SSR setup promise on request variables, so getSSRSetup returns it
|
|
366
|
+
// without falling back to a fresh startSSRSetup.
|
|
367
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
368
|
+
ctx,
|
|
369
|
+
request,
|
|
370
|
+
env,
|
|
371
|
+
url,
|
|
372
|
+
undefined,
|
|
373
|
+
);
|
|
357
374
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
358
375
|
nonce,
|
|
359
376
|
streamMode,
|
package/src/rsc/rsc-rendering.ts
CHANGED
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
getLocationState,
|
|
13
13
|
} from "../server/request-context.js";
|
|
14
14
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
15
|
+
import { appendMetric } from "../router/metrics.js";
|
|
16
|
+
import { getSSRSetup } from "./ssr-setup.js";
|
|
15
17
|
import type { RscPayload } from "./types.js";
|
|
16
18
|
import {
|
|
17
19
|
createResponseWithMergedHeaders,
|
|
@@ -28,13 +30,9 @@ export async function handleRscRendering<TEnv>(
|
|
|
28
30
|
handleStore: ReturnType<typeof requireRequestContext>["_handleStore"],
|
|
29
31
|
nonce: string | undefined,
|
|
30
32
|
): Promise<Response> {
|
|
31
|
-
// Retrieve handler-level timing from variables
|
|
32
33
|
const reqCtx = requireRequestContext();
|
|
33
|
-
const handlerTimingArr: string[] = reqCtx.var.__handlerTiming || [];
|
|
34
|
-
const handlerStart: number = reqCtx.var.__handlerStart || 0;
|
|
35
34
|
|
|
36
35
|
let payload: RscPayload;
|
|
37
|
-
let serverTiming: string | undefined;
|
|
38
36
|
let hasInterceptSlots = false;
|
|
39
37
|
|
|
40
38
|
if (isPartial) {
|
|
@@ -53,11 +51,11 @@ export async function handleRscRendering<TEnv>(
|
|
|
53
51
|
return createSimpleRedirectResponse(match.redirect);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
|
-
serverTiming = match.serverTiming;
|
|
57
|
-
|
|
58
54
|
payload = {
|
|
59
55
|
metadata: {
|
|
60
56
|
pathname: url.pathname,
|
|
57
|
+
routerId: ctx.router.id,
|
|
58
|
+
basename: ctx.router.basename,
|
|
61
59
|
segments: match.segments,
|
|
62
60
|
matched: match.matched,
|
|
63
61
|
diff: match.diff,
|
|
@@ -66,18 +64,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
66
64
|
rootLayout: ctx.router.rootLayout,
|
|
67
65
|
handles: handleStore.stream(),
|
|
68
66
|
version: ctx.version,
|
|
67
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
69
68
|
themeConfig: ctx.router.themeConfig,
|
|
70
69
|
initialTheme: reqCtx.theme,
|
|
71
70
|
},
|
|
72
71
|
};
|
|
73
72
|
} else {
|
|
74
73
|
setRequestContextParams(result.params, result.routeName);
|
|
75
|
-
|
|
74
|
+
|
|
76
75
|
hasInterceptSlots = !!result.slots;
|
|
77
76
|
|
|
78
77
|
payload = {
|
|
79
78
|
metadata: {
|
|
80
79
|
pathname: url.pathname,
|
|
80
|
+
routerId: ctx.router.id,
|
|
81
81
|
segments: result.segments,
|
|
82
82
|
matched: result.matched,
|
|
83
83
|
diff: result.diff,
|
|
@@ -86,6 +86,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
86
86
|
slots: result.slots,
|
|
87
87
|
handles: handleStore.stream(),
|
|
88
88
|
version: ctx.version,
|
|
89
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
89
90
|
},
|
|
90
91
|
};
|
|
91
92
|
}
|
|
@@ -132,14 +133,14 @@ export async function handleRscRendering<TEnv>(
|
|
|
132
133
|
{ headers: { "Content-Type": "application/json" } },
|
|
133
134
|
);
|
|
134
135
|
} else {
|
|
135
|
-
serverTiming = match.serverTiming;
|
|
136
|
-
|
|
137
136
|
payload = {
|
|
138
137
|
// Initial SSR can reconstruct the tree from segments + rootLayout,
|
|
139
138
|
// so we omit root to avoid sending the same structure twice.
|
|
140
139
|
|
|
141
140
|
metadata: {
|
|
142
141
|
pathname: url.pathname,
|
|
142
|
+
routerId: ctx.router.id,
|
|
143
|
+
basename: ctx.router.basename,
|
|
143
144
|
segments: match.segments,
|
|
144
145
|
matched: match.matched,
|
|
145
146
|
diff: match.diff,
|
|
@@ -148,6 +149,7 @@ export async function handleRscRendering<TEnv>(
|
|
|
148
149
|
rootLayout: ctx.router.rootLayout,
|
|
149
150
|
handles: handleStore.stream(),
|
|
150
151
|
version: ctx.version,
|
|
152
|
+
prefetchCacheTTL: ctx.router.prefetchCacheTTL,
|
|
151
153
|
themeConfig: ctx.router.themeConfig,
|
|
152
154
|
initialTheme: reqCtx.theme,
|
|
153
155
|
},
|
|
@@ -166,10 +168,20 @@ export async function handleRscRendering<TEnv>(
|
|
|
166
168
|
}
|
|
167
169
|
}
|
|
168
170
|
|
|
171
|
+
const metricsStore = reqCtx._metricsStore;
|
|
172
|
+
const renderStart = performance.now();
|
|
173
|
+
|
|
169
174
|
// Serialize to RSC stream
|
|
170
175
|
const rscSerializeStart = performance.now();
|
|
171
176
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload);
|
|
172
177
|
const rscSerializeDur = performance.now() - rscSerializeStart;
|
|
178
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
179
|
+
appendMetric(
|
|
180
|
+
metricsStore,
|
|
181
|
+
"rsc-serialize",
|
|
182
|
+
rscSerializeStart,
|
|
183
|
+
rscSerializeDur,
|
|
184
|
+
);
|
|
173
185
|
|
|
174
186
|
// Determine if this is an RSC request or HTML request.
|
|
175
187
|
// Partial requests (_rsc_partial) are always RSC -- they come from client-side
|
|
@@ -181,15 +193,9 @@ export async function handleRscRendering<TEnv>(
|
|
|
181
193
|
!url.searchParams.has("__html")) ||
|
|
182
194
|
url.searchParams.has("__rsc");
|
|
183
195
|
|
|
184
|
-
// Build complete Server-Timing: handler phases + match/manifest + RSC serialize
|
|
185
|
-
const timingParts: string[] = [...handlerTimingArr];
|
|
186
|
-
if (serverTiming) {
|
|
187
|
-
timingParts.push(serverTiming);
|
|
188
|
-
}
|
|
189
|
-
timingParts.push(`rsc-serialize;dur=${rscSerializeDur.toFixed(2)}`);
|
|
190
|
-
|
|
191
196
|
if (isRscRequest) {
|
|
192
|
-
const
|
|
197
|
+
const renderDur = performance.now() - renderStart;
|
|
198
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
193
199
|
const rscHeaders: Record<string, string> = {
|
|
194
200
|
"content-type": "text/x-component;charset=utf-8",
|
|
195
201
|
vary: "accept, X-Rango-State, X-RSC-Router-Client-Path",
|
|
@@ -205,22 +211,19 @@ export async function handleRscRendering<TEnv>(
|
|
|
205
211
|
rscHeaders["cache-control"] = cc;
|
|
206
212
|
}
|
|
207
213
|
}
|
|
208
|
-
if (fullTiming) {
|
|
209
|
-
rscHeaders["Server-Timing"] = fullTiming;
|
|
210
|
-
}
|
|
211
214
|
return createResponseWithMergedHeaders(rscStream, {
|
|
212
215
|
headers: rscHeaders,
|
|
213
216
|
});
|
|
214
217
|
}
|
|
215
218
|
|
|
216
|
-
// Delegate to SSR for HTML response
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
219
|
+
// Delegate to SSR for HTML response (reuse early setup if available)
|
|
220
|
+
const [ssrModule, streamMode] = await getSSRSetup(
|
|
221
|
+
ctx,
|
|
222
|
+
request,
|
|
223
|
+
env,
|
|
224
|
+
url,
|
|
225
|
+
metricsStore,
|
|
226
|
+
);
|
|
224
227
|
|
|
225
228
|
const ssrRenderStart = performance.now();
|
|
226
229
|
const htmlStream = await ssrModule.renderHTML(rscStream, {
|
|
@@ -228,23 +231,12 @@ export async function handleRscRendering<TEnv>(
|
|
|
228
231
|
streamMode,
|
|
229
232
|
});
|
|
230
233
|
const ssrRenderDur = performance.now() - ssrRenderStart;
|
|
231
|
-
|
|
234
|
+
appendMetric(metricsStore, "ssr-render-html", ssrRenderStart, ssrRenderDur);
|
|
232
235
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const totalHandler = performance.now() - handlerStart;
|
|
236
|
-
timingParts.push(`handler-total;dur=${totalHandler.toFixed(2)}`);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const fullTiming = timingParts.join(", ");
|
|
240
|
-
const htmlHeaders: Record<string, string> = {
|
|
241
|
-
"content-type": "text/html;charset=utf-8",
|
|
242
|
-
};
|
|
243
|
-
if (fullTiming) {
|
|
244
|
-
htmlHeaders["Server-Timing"] = fullTiming;
|
|
245
|
-
}
|
|
236
|
+
const renderDur = performance.now() - renderStart;
|
|
237
|
+
appendMetric(metricsStore, "render:total", renderStart, renderDur);
|
|
246
238
|
|
|
247
239
|
return createResponseWithMergedHeaders(htmlStream, {
|
|
248
|
-
headers:
|
|
240
|
+
headers: { "content-type": "text/html;charset=utf-8" },
|
|
249
241
|
});
|
|
250
242
|
}
|
package/src/rsc/server-action.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
getLocationState,
|
|
22
22
|
} from "../server/request-context.js";
|
|
23
23
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
24
|
+
import { appendMetric } from "../router/metrics.js";
|
|
24
25
|
import type { RscPayload } from "./types.js";
|
|
25
26
|
import {
|
|
26
27
|
hasBodyContent,
|
|
@@ -207,6 +208,7 @@ export async function executeServerAction<TEnv>(
|
|
|
207
208
|
const payload: RscPayload = {
|
|
208
209
|
metadata: {
|
|
209
210
|
pathname: url.pathname,
|
|
211
|
+
routerId: ctx.router.id,
|
|
210
212
|
segments: errorResult.segments,
|
|
211
213
|
isPartial: true,
|
|
212
214
|
matched: errorResult.matched,
|
|
@@ -274,6 +276,8 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
274
276
|
): Promise<Response> {
|
|
275
277
|
const { returnValue, actionStatus, temporaryReferences, actionContext } =
|
|
276
278
|
continuation;
|
|
279
|
+
const reqCtx = requireRequestContext();
|
|
280
|
+
const metricsStore = reqCtx._metricsStore;
|
|
277
281
|
|
|
278
282
|
const matchResult = await ctx.router.matchPartial(
|
|
279
283
|
request,
|
|
@@ -308,11 +312,10 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
308
312
|
// Return updated segments
|
|
309
313
|
setRequestContextParams(matchResult.params, matchResult.routeName);
|
|
310
314
|
|
|
311
|
-
const serverTiming = matchResult.serverTiming;
|
|
312
|
-
|
|
313
315
|
const payload: RscPayload = {
|
|
314
316
|
metadata: {
|
|
315
317
|
pathname: url.pathname,
|
|
318
|
+
routerId: ctx.router.id,
|
|
316
319
|
segments: matchResult.segments,
|
|
317
320
|
isPartial: true,
|
|
318
321
|
matched: matchResult.matched,
|
|
@@ -326,19 +329,22 @@ export async function revalidateAfterAction<TEnv>(
|
|
|
326
329
|
|
|
327
330
|
attachLocationState(payload);
|
|
328
331
|
|
|
332
|
+
const renderStart = performance.now();
|
|
329
333
|
const rscStream = ctx.renderToReadableStream<RscPayload>(payload, {
|
|
330
334
|
temporaryReferences,
|
|
331
335
|
});
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
336
|
+
const rscSerializeDur = performance.now() - renderStart;
|
|
337
|
+
// This measures synchronous stream creation, not end-to-end stream consumption.
|
|
338
|
+
appendMetric(metricsStore, "rsc-serialize", renderStart, rscSerializeDur);
|
|
339
|
+
appendMetric(
|
|
340
|
+
metricsStore,
|
|
341
|
+
"render:total",
|
|
342
|
+
renderStart,
|
|
343
|
+
performance.now() - renderStart,
|
|
344
|
+
);
|
|
339
345
|
|
|
340
346
|
return createResponseWithMergedHeaders(rscStream, {
|
|
341
347
|
status: actionStatus,
|
|
342
|
-
headers:
|
|
348
|
+
headers: { "content-type": "text/x-component;charset=utf-8" },
|
|
343
349
|
});
|
|
344
350
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SSR Setup Utilities
|
|
3
|
+
*
|
|
4
|
+
* Manages early kickoff and retrieval of SSR module loading and stream mode
|
|
5
|
+
* resolution. Both operations are request-scoped but independent of route
|
|
6
|
+
* matching, so they can run in parallel with segment resolution.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { HandlerContext } from "./handler-context.js";
|
|
10
|
+
import type { SSRModule } from "./types.js";
|
|
11
|
+
import type { SSRStreamMode } from "../router/router-options.js";
|
|
12
|
+
import type { MetricsStore } from "../server/context.js";
|
|
13
|
+
import { appendMetric } from "../router/metrics.js";
|
|
14
|
+
import { _getRequestContext } from "../server/request-context.js";
|
|
15
|
+
|
|
16
|
+
export type SSRSetup = readonly [SSRModule, SSRStreamMode];
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Key used to stash the early SSR setup promise on request variables.
|
|
20
|
+
* Read back via `getSSRSetup`.
|
|
21
|
+
*/
|
|
22
|
+
export const SSR_SETUP_VAR = "__ssrSetup";
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start loading the SSR module and resolving the stream mode in parallel.
|
|
26
|
+
* When a `getMetricsStore` getter is provided, records individual
|
|
27
|
+
* `ssr:module-load` and `ssr:stream-mode` metrics (the getter is called
|
|
28
|
+
* lazily so stores created after kickoff are still captured). Without a
|
|
29
|
+
* getter the promises run bare — no `.then()` microtasks, no
|
|
30
|
+
* `performance.now()` calls — keeping the non-debug hot path lean.
|
|
31
|
+
*/
|
|
32
|
+
export function startSSRSetup<TEnv>(
|
|
33
|
+
ctx: HandlerContext<TEnv>,
|
|
34
|
+
request: Request,
|
|
35
|
+
env: TEnv,
|
|
36
|
+
url: URL,
|
|
37
|
+
getMetricsStore?: () => MetricsStore | undefined,
|
|
38
|
+
): Promise<SSRSetup> {
|
|
39
|
+
if (!getMetricsStore) {
|
|
40
|
+
return Promise.all([
|
|
41
|
+
ctx.loadSSRModule(),
|
|
42
|
+
ctx.resolveStreamMode(request, env, url),
|
|
43
|
+
]);
|
|
44
|
+
}
|
|
45
|
+
const start = performance.now();
|
|
46
|
+
return Promise.all([
|
|
47
|
+
ctx.loadSSRModule().then((mod) => {
|
|
48
|
+
appendMetric(
|
|
49
|
+
getMetricsStore(),
|
|
50
|
+
"ssr:module-load",
|
|
51
|
+
start,
|
|
52
|
+
performance.now() - start,
|
|
53
|
+
);
|
|
54
|
+
return mod;
|
|
55
|
+
}),
|
|
56
|
+
ctx.resolveStreamMode(request, env, url).then((mode) => {
|
|
57
|
+
appendMetric(
|
|
58
|
+
getMetricsStore(),
|
|
59
|
+
"ssr:stream-mode",
|
|
60
|
+
start,
|
|
61
|
+
performance.now() - start,
|
|
62
|
+
);
|
|
63
|
+
return mode;
|
|
64
|
+
}),
|
|
65
|
+
]);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the SSR setup result. Returns the early-kicked-off promise
|
|
70
|
+
* when available (stashed on request variables), otherwise starts a
|
|
71
|
+
* fresh setup.
|
|
72
|
+
*/
|
|
73
|
+
export function getSSRSetup<TEnv>(
|
|
74
|
+
ctx: HandlerContext<TEnv>,
|
|
75
|
+
request: Request,
|
|
76
|
+
env: TEnv,
|
|
77
|
+
url: URL,
|
|
78
|
+
metricsStore: MetricsStore | undefined,
|
|
79
|
+
): Promise<SSRSetup> {
|
|
80
|
+
const early = _getRequestContext()?._variables?.[SSR_SETUP_VAR] as
|
|
81
|
+
| Promise<SSRSetup>
|
|
82
|
+
| undefined;
|
|
83
|
+
if (early) return early;
|
|
84
|
+
return startSSRSetup(
|
|
85
|
+
ctx,
|
|
86
|
+
request,
|
|
87
|
+
env,
|
|
88
|
+
url,
|
|
89
|
+
metricsStore ? () => metricsStore : undefined,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Classify whether a request may require SSR (HTML rendering).
|
|
95
|
+
*
|
|
96
|
+
* Returns false for requests that are definitively RSC-only, loader fetches,
|
|
97
|
+
* prerender collection, or Accept-based RSC (no text/html). This mirrors
|
|
98
|
+
* the isRscRequest decision in rsc-rendering.ts.
|
|
99
|
+
*
|
|
100
|
+
* Note: response/mime routes are excluded by the caller — this function
|
|
101
|
+
* runs after classifyRequest() determines the request mode.
|
|
102
|
+
*/
|
|
103
|
+
export function mayNeedSSR(request: Request, url: URL): boolean {
|
|
104
|
+
if (
|
|
105
|
+
url.searchParams.has("_rsc_partial") ||
|
|
106
|
+
url.searchParams.has("_rsc_action") ||
|
|
107
|
+
request.headers.has("rsc-action") ||
|
|
108
|
+
url.searchParams.has("_rsc_loader") ||
|
|
109
|
+
url.searchParams.has("__rsc") ||
|
|
110
|
+
url.searchParams.has("__prerender_collect")
|
|
111
|
+
) {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Mirror the Accept-based RSC decision from rsc-rendering.ts:
|
|
116
|
+
// if Accept is present and does not include text/html (and no __html override),
|
|
117
|
+
// the response will be RSC, not HTML.
|
|
118
|
+
const accept = request.headers.get("accept");
|
|
119
|
+
if (
|
|
120
|
+
accept &&
|
|
121
|
+
!accept.includes("text/html") &&
|
|
122
|
+
!url.searchParams.has("__html")
|
|
123
|
+
) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
}
|
package/src/rsc/types.ts
CHANGED
|
@@ -19,6 +19,9 @@ export interface RscPayload {
|
|
|
19
19
|
metadata?: {
|
|
20
20
|
pathname: string;
|
|
21
21
|
segments: ResolvedSegment[];
|
|
22
|
+
/** Router instance ID. When this changes between navigations, the client
|
|
23
|
+
* discards cached segments and does a full tree replacement (app switch). */
|
|
24
|
+
routerId?: string;
|
|
22
25
|
isPartial?: boolean;
|
|
23
26
|
isError?: boolean;
|
|
24
27
|
matched?: string[];
|
|
@@ -32,10 +35,14 @@ export interface RscPayload {
|
|
|
32
35
|
handles?: AsyncGenerator<HandleData, void, unknown>;
|
|
33
36
|
/** RSC version string for cache invalidation */
|
|
34
37
|
version?: string;
|
|
38
|
+
/** TTL in milliseconds for the client-side in-memory prefetch cache */
|
|
39
|
+
prefetchCacheTTL?: number;
|
|
35
40
|
/** Theme configuration for FOUC prevention */
|
|
36
41
|
themeConfig?: ResolvedThemeConfig | null;
|
|
37
42
|
/** Initial theme from cookie (for SSR hydration) */
|
|
38
43
|
initialTheme?: Theme;
|
|
44
|
+
/** URL prefix for all routes (from createRouter({ basename })). */
|
|
45
|
+
basename?: string;
|
|
39
46
|
/** Whether connection warmup is enabled */
|
|
40
47
|
warmupEnabled?: boolean;
|
|
41
48
|
/** Server-side redirect with optional state (for partial requests) */
|
|
@@ -61,7 +68,9 @@ export interface RSCDependencies {
|
|
|
61
68
|
*/
|
|
62
69
|
renderToReadableStream: <T>(
|
|
63
70
|
payload: T,
|
|
64
|
-
options?: {
|
|
71
|
+
options?: {
|
|
72
|
+
temporaryReferences?: unknown;
|
|
73
|
+
},
|
|
65
74
|
) => ReadableStream<Uint8Array>;
|
|
66
75
|
|
|
67
76
|
/**
|
package/src/search-params.ts
CHANGED
|
@@ -55,14 +55,22 @@ type Simplify<T> = { [K in keyof T]: T[K] };
|
|
|
55
55
|
/**
|
|
56
56
|
* Resolve a SearchSchema to its typed object.
|
|
57
57
|
*
|
|
58
|
+
* Both required and optional params resolve to `T | undefined` at the handler
|
|
59
|
+
* level. The required/optional distinction is a consumer-facing contract
|
|
60
|
+
* (e.g., for href() and reverse() autocomplete) — it tells callers which
|
|
61
|
+
* params the route expects, but the handler must still check for undefined
|
|
62
|
+
* since the framework cannot trust the client to send all required params.
|
|
63
|
+
*
|
|
58
64
|
* @example
|
|
59
65
|
* type S = { q: "string"; page: "number?"; sort: "string?" };
|
|
60
66
|
* type R = ResolveSearchSchema<S>;
|
|
61
|
-
* // { q: string; page?: number; sort?: string }
|
|
67
|
+
* // { q: string | undefined; page?: number; sort?: string }
|
|
62
68
|
*/
|
|
63
69
|
export type ResolveSearchSchema<T extends SearchSchema> = Simplify<
|
|
64
70
|
{
|
|
65
|
-
[K in RequiredKeys<T> & string]:
|
|
71
|
+
[K in RequiredKeys<T> & string]:
|
|
72
|
+
| ResolveBaseType<BaseType<T[K]>>
|
|
73
|
+
| undefined;
|
|
66
74
|
} & {
|
|
67
75
|
[K in OptionalKeys<T> & string]?: ResolveBaseType<BaseType<T[K]>>;
|
|
68
76
|
}
|
|
@@ -166,7 +174,9 @@ type ExtractParamsFromPattern<T extends string> =
|
|
|
166
174
|
* - `"number"` / `"number?"` - coerced via `Number()`; NaN treated as missing
|
|
167
175
|
* - `"boolean"` / `"boolean?"` - `"true"` / `"1"` -> true, `"false"` / `"0"` / `""` -> false
|
|
168
176
|
*
|
|
169
|
-
* Missing
|
|
177
|
+
* Missing params (both required and optional) are omitted from the result
|
|
178
|
+
* (undefined). The required/optional distinction is a consumer-facing contract
|
|
179
|
+
* only — the handler must check for undefined.
|
|
170
180
|
*/
|
|
171
181
|
export function parseSearchParams<T extends SearchSchema>(
|
|
172
182
|
searchParams: URLSearchParams,
|
|
@@ -180,13 +190,7 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
180
190
|
const raw = searchParams.get(key);
|
|
181
191
|
|
|
182
192
|
if (raw === null) {
|
|
183
|
-
|
|
184
|
-
// Required param missing: use zero value
|
|
185
|
-
if (baseType === "string") result[key] = "";
|
|
186
|
-
else if (baseType === "number") result[key] = 0;
|
|
187
|
-
else if (baseType === "boolean") result[key] = false;
|
|
188
|
-
}
|
|
189
|
-
// Optional params are omitted (undefined)
|
|
193
|
+
// Missing params are omitted (undefined) regardless of required/optional
|
|
190
194
|
continue;
|
|
191
195
|
}
|
|
192
196
|
|
|
@@ -194,11 +198,10 @@ export function parseSearchParams<T extends SearchSchema>(
|
|
|
194
198
|
result[key] = raw;
|
|
195
199
|
} else if (baseType === "number") {
|
|
196
200
|
const num = Number(raw);
|
|
197
|
-
if (Number.isNaN(num)) {
|
|
198
|
-
if (!isOptional) result[key] = 0;
|
|
199
|
-
} else {
|
|
201
|
+
if (!Number.isNaN(num)) {
|
|
200
202
|
result[key] = num;
|
|
201
203
|
}
|
|
204
|
+
// NaN treated as missing (undefined)
|
|
202
205
|
} else if (baseType === "boolean") {
|
|
203
206
|
result[key] = raw === "true" || raw === "1";
|
|
204
207
|
}
|