@rangojs/router 0.0.0-experimental.fb4fdc18 → 0.0.0-experimental.fce7fbd1
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/README.md +9 -9
- package/dist/bin/rango.js +147 -57
- package/dist/testing/vitest.js +48 -0
- package/dist/vite/index.js +914 -485
- package/package.json +55 -11
- package/skills/bundle-analysis/SKILL.md +159 -0
- package/skills/cache-guide/SKILL.md +220 -30
- package/skills/caching/SKILL.md +116 -8
- package/skills/composability/SKILL.md +27 -2
- package/skills/document-cache/SKILL.md +78 -55
- package/skills/handler-use/SKILL.md +3 -1
- package/skills/hooks/SKILL.md +214 -18
- package/skills/host-router/SKILL.md +45 -20
- package/skills/intercept/SKILL.md +26 -4
- package/skills/layout/SKILL.md +6 -7
- package/skills/links/SKILL.md +173 -17
- package/skills/loader/SKILL.md +149 -6
- package/skills/middleware/SKILL.md +13 -9
- package/skills/migrate-nextjs/SKILL.md +1 -1
- package/skills/mime-routes/SKILL.md +27 -0
- package/skills/observability/SKILL.md +137 -0
- package/skills/parallel/SKILL.md +5 -6
- package/skills/prerender/SKILL.md +14 -33
- package/skills/rango/SKILL.md +242 -26
- package/skills/react-compiler/SKILL.md +168 -0
- package/skills/response-routes/SKILL.md +58 -9
- package/skills/route/SKILL.md +13 -4
- package/skills/router-setup/SKILL.md +3 -3
- package/skills/server-actions/SKILL.md +53 -41
- package/skills/testing/SKILL.md +599 -0
- package/skills/typesafety/SKILL.md +310 -26
- package/skills/use-cache/SKILL.md +34 -5
- package/skills/view-transitions/SKILL.md +294 -0
- package/src/__augment-tests__/augment.ts +81 -0
- package/src/__augment-tests__/augmented.check.ts +117 -0
- package/src/browser/action-coordinator.ts +53 -36
- package/src/browser/event-controller.ts +42 -66
- package/src/browser/history-state.ts +21 -0
- package/src/browser/index.ts +3 -3
- package/src/browser/navigation-bridge.ts +6 -6
- package/src/browser/navigation-client.ts +12 -15
- package/src/browser/navigation-store.ts +7 -8
- package/src/browser/navigation-transaction.ts +10 -28
- package/src/browser/partial-update.ts +9 -19
- package/src/browser/react/NavigationProvider.tsx +29 -40
- package/src/browser/react/index.ts +3 -0
- package/src/browser/react/location-state-shared.ts +175 -4
- package/src/browser/react/location-state.ts +39 -13
- package/src/browser/react/use-handle.ts +17 -9
- package/src/browser/react/use-params.ts +3 -4
- package/src/browser/react/use-reverse.ts +106 -0
- package/src/browser/react/use-router.ts +14 -1
- package/src/browser/response-adapter.ts +25 -0
- package/src/browser/rsc-router.tsx +30 -16
- package/src/browser/scroll-restoration.ts +22 -14
- package/src/browser/segment-structure-assert.ts +2 -2
- package/src/browser/server-action-bridge.ts +23 -30
- package/src/browser/types.ts +2 -0
- package/src/build/collect-fallback-refs.ts +107 -0
- package/src/build/generate-manifest.ts +60 -35
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/index.ts +2 -0
- package/src/build/route-types/codegen.ts +4 -4
- package/src/build/route-types/include-resolution.ts +1 -1
- package/src/build/route-types/per-module-writer.ts +7 -4
- package/src/build/route-types/router-processing.ts +55 -14
- package/src/build/route-types/scan-filter.ts +1 -1
- package/src/build/route-types/source-scan.ts +118 -0
- package/src/build/runtime-discovery.ts +9 -20
- package/src/cache/cache-scope.ts +28 -42
- package/src/cache/cf/cf-cache-store.ts +49 -6
- package/src/client.rsc.tsx +3 -0
- package/src/client.tsx +10 -8
- package/src/context-var.ts +5 -5
- package/src/decode-loader-results.ts +36 -0
- package/src/errors.ts +30 -1
- package/src/handle.ts +26 -13
- package/src/host/index.ts +2 -2
- package/src/host/router.ts +129 -57
- package/src/host/types.ts +31 -2
- package/src/host/utils.ts +1 -1
- package/src/href-client.ts +140 -20
- package/src/index.rsc.ts +6 -4
- package/src/index.ts +13 -6
- package/src/loader-store.ts +500 -0
- package/src/loader.rsc.ts +2 -5
- package/src/loader.ts +3 -10
- package/src/missing-id-error.ts +68 -0
- package/src/prerender.ts +4 -4
- package/src/response-utils.ts +9 -0
- package/src/reverse.ts +65 -41
- package/src/route-content-wrapper.tsx +6 -28
- package/src/route-definition/dsl-helpers.ts +238 -263
- package/src/route-definition/helper-factories.ts +29 -139
- package/src/route-definition/helpers-types.ts +37 -14
- package/src/route-definition/use-item-types.ts +32 -0
- package/src/route-types.ts +19 -41
- package/src/router/basename.ts +14 -0
- package/src/router/content-negotiation.ts +15 -2
- package/src/router/error-handling.ts +1 -1
- package/src/router/handler-context.ts +4 -42
- package/src/router/intercept-resolution.ts +4 -18
- package/src/router/lazy-includes.ts +2 -2
- package/src/router/loader-resolution.ts +16 -2
- package/src/router/match-handlers.ts +62 -20
- package/src/router/match-middleware/cache-lookup.ts +44 -91
- package/src/router/match-middleware/cache-store.ts +3 -2
- package/src/router/match-result.ts +32 -30
- package/src/router/metrics.ts +1 -1
- package/src/router/middleware-types.ts +1 -1
- package/src/router/middleware.ts +46 -78
- package/src/router/prerender-match.ts +1 -1
- package/src/router/preview-match.ts +3 -1
- package/src/router/request-classification.ts +4 -28
- package/src/router/revalidation.ts +43 -1
- package/src/router/router-interfaces.ts +45 -28
- package/src/router/router-options.ts +40 -1
- package/src/router/router-registry.ts +2 -5
- package/src/router/segment-resolution/fresh.ts +19 -6
- package/src/router/segment-resolution/revalidation.ts +19 -6
- package/src/router/segment-resolution/view-transition-default.ts +36 -0
- package/src/router/substitute-pattern-params.ts +56 -0
- package/src/router/telemetry.ts +99 -0
- package/src/router/types.ts +8 -0
- package/src/router.ts +37 -21
- package/src/rsc/handler-context.ts +2 -2
- package/src/rsc/handler.ts +20 -65
- package/src/rsc/helpers.ts +22 -2
- package/src/rsc/index.ts +1 -1
- package/src/rsc/origin-guard.ts +28 -10
- package/src/rsc/response-route-handler.ts +32 -52
- package/src/rsc/rsc-rendering.ts +27 -53
- package/src/rsc/runtime-warnings.ts +9 -10
- package/src/rsc/server-action.ts +13 -37
- package/src/rsc/ssr-setup.ts +16 -0
- package/src/rsc/types.ts +2 -2
- package/src/search-params.ts +4 -4
- package/src/segment-system.tsx +121 -65
- package/src/serialize.ts +243 -0
- package/src/server/context.ts +118 -51
- package/src/server/cookie-store.ts +28 -4
- package/src/server/request-context.ts +10 -0
- package/src/static-handler.ts +1 -1
- package/src/testing/cache-status.ts +166 -0
- package/src/testing/collect-handle.ts +63 -0
- package/src/testing/dispatch.ts +440 -0
- package/src/testing/dom.entry.ts +22 -0
- package/src/testing/e2e/fixture.ts +154 -0
- package/src/testing/e2e/index.ts +149 -0
- package/src/testing/e2e/matchers.ts +51 -0
- package/src/testing/e2e/page-helpers.ts +272 -0
- package/src/testing/e2e/parity.ts +306 -0
- package/src/testing/e2e/server.ts +183 -0
- package/src/testing/flight-matchers.ts +104 -0
- package/src/testing/flight-runtime.d.ts +21 -0
- package/src/testing/flight.entry.ts +22 -0
- package/src/testing/flight.ts +182 -0
- package/src/testing/generated-routes.ts +223 -0
- package/src/testing/index.ts +105 -0
- package/src/testing/internal/context.ts +193 -0
- package/src/testing/render-route.tsx +536 -0
- package/src/testing/run-loader.ts +296 -0
- package/src/testing/run-middleware.ts +170 -0
- package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
- package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
- package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
- package/src/testing/vitest-stubs/version.ts +5 -0
- package/src/testing/vitest.ts +183 -0
- package/src/types/global-namespace.ts +39 -26
- package/src/types/handler-context.ts +56 -11
- package/src/types/index.ts +1 -0
- package/src/types/segments.ts +18 -1
- package/src/urls/include-helper.ts +10 -53
- package/src/urls/index.ts +0 -3
- package/src/urls/path-helper-types.ts +11 -3
- package/src/urls/path-helper.ts +17 -52
- package/src/urls/pattern-types.ts +36 -19
- package/src/urls/response-types.ts +20 -19
- package/src/urls/type-extraction.ts +26 -116
- package/src/urls/urls-function.ts +1 -5
- package/src/use-loader.tsx +413 -42
- package/src/vite/debug.ts +1 -0
- package/src/vite/discovery/bundle-postprocess.ts +6 -6
- package/src/vite/discovery/discover-routers.ts +70 -48
- package/src/vite/discovery/discovery-errors.ts +194 -0
- package/src/vite/discovery/prerender-collection.ts +19 -25
- package/src/vite/discovery/route-types-writer.ts +40 -84
- package/src/vite/discovery/state.ts +33 -0
- package/src/vite/discovery/virtual-module-codegen.ts +13 -23
- package/src/vite/index.ts +2 -0
- package/src/vite/plugin-types.ts +67 -0
- package/src/vite/plugins/cjs-to-esm.ts +3 -7
- package/src/vite/plugins/client-ref-hashing.ts +12 -1
- package/src/vite/plugins/cloudflare-protocol-stub.ts +1 -1
- package/src/vite/plugins/expose-action-id.ts +2 -2
- package/src/vite/plugins/expose-id-utils.ts +12 -8
- package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
- package/src/vite/plugins/expose-ids/handler-transform.ts +8 -61
- package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
- package/src/vite/plugins/expose-internal-ids.ts +47 -67
- package/src/vite/plugins/performance-tracks.ts +12 -16
- package/src/vite/plugins/use-cache-transform.ts +13 -11
- package/src/vite/plugins/version-injector.ts +2 -12
- package/src/vite/plugins/version-plugin.ts +59 -2
- package/src/vite/plugins/virtual-entries.ts +2 -2
- package/src/vite/rango.ts +67 -15
- package/src/vite/router-discovery.ts +208 -63
- package/src/vite/utils/ast-handler-extract.ts +15 -15
- package/src/vite/utils/bundle-analysis.ts +4 -2
- package/src/vite/utils/client-chunks.ts +190 -0
- package/src/vite/utils/forward-user-plugins.ts +193 -0
- package/src/vite/utils/manifest-utils.ts +21 -5
- package/src/vite/utils/shared-utils.ts +107 -26
- package/src/browser/action-response-classifier.ts +0 -99
package/src/router/telemetry.ts
CHANGED
|
@@ -90,6 +90,34 @@ export interface HandlerErrorEvent extends BaseEvent {
|
|
|
90
90
|
params?: Record<string, string>;
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Per-segment (or coarse route-level) cache status carried on the
|
|
95
|
+
* cache.decision telemetry event and the X-Rango-Cache debug header.
|
|
96
|
+
*
|
|
97
|
+
* v1 is COARSE: the router's pipeline tracks cache decisions at the
|
|
98
|
+
* route/entry level (cacheHit/cacheSource/shouldRevalidate), not per
|
|
99
|
+
* individual segment. The `segments` array therefore contains a single
|
|
100
|
+
* route-level entry keyed by the route key. The shape is forward-compatible
|
|
101
|
+
* with genuine per-segment status if the pipeline later exposes it.
|
|
102
|
+
*/
|
|
103
|
+
export type CacheSegmentStatus =
|
|
104
|
+
| "hit"
|
|
105
|
+
| "miss"
|
|
106
|
+
| "stale"
|
|
107
|
+
| "prerendered"
|
|
108
|
+
| "passthrough";
|
|
109
|
+
|
|
110
|
+
export interface CacheSegmentSignal {
|
|
111
|
+
/** Segment id (v1: the route key, since status is route-level). */
|
|
112
|
+
id: string;
|
|
113
|
+
/** Segment type (v1: "route" for the coarse route-level entry). */
|
|
114
|
+
type: string;
|
|
115
|
+
/** Resolved cache status for this segment. */
|
|
116
|
+
cacheStatus: CacheSegmentStatus;
|
|
117
|
+
/** Whether stale-while-revalidate was triggered for this segment. */
|
|
118
|
+
shouldRevalidate?: boolean;
|
|
119
|
+
}
|
|
120
|
+
|
|
93
121
|
export interface CacheDecisionEvent extends BaseEvent {
|
|
94
122
|
type: "cache.decision";
|
|
95
123
|
pathname: string;
|
|
@@ -98,6 +126,12 @@ export interface CacheDecisionEvent extends BaseEvent {
|
|
|
98
126
|
/** Whether stale-while-revalidate was triggered */
|
|
99
127
|
shouldRevalidate: boolean;
|
|
100
128
|
source?: "runtime" | "prerender";
|
|
129
|
+
/**
|
|
130
|
+
* Optional per-segment (v1: coarse route-level) cache status. Present only
|
|
131
|
+
* when telemetry or the debug cache signal is enabled. Optional so existing
|
|
132
|
+
* sinks are unaffected.
|
|
133
|
+
*/
|
|
134
|
+
segments?: CacheSegmentSignal[];
|
|
101
135
|
}
|
|
102
136
|
|
|
103
137
|
export interface RevalidationDecisionEvent extends BaseEvent {
|
|
@@ -140,6 +174,71 @@ export type TelemetryEvent =
|
|
|
140
174
|
| RequestTimeoutEvent
|
|
141
175
|
| OriginCheckRejectedEvent;
|
|
142
176
|
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Cache signal derivation (coarse, route-level)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Derive the coarse, route-level cache status from pipeline cache state.
|
|
183
|
+
*
|
|
184
|
+
* v1 mapping (route-level — see CacheSegmentSignal):
|
|
185
|
+
* - prerender hit -> "prerendered"
|
|
186
|
+
* - runtime hit + shouldRevalidate (SWR) -> "stale"
|
|
187
|
+
* - runtime hit -> "hit"
|
|
188
|
+
* - no hit -> "miss"
|
|
189
|
+
*
|
|
190
|
+
* Note: "passthrough" is a build-time prerender concept (a route opts out of
|
|
191
|
+
* being prerendered for some params). At runtime a passthrough route renders
|
|
192
|
+
* fresh and is indistinguishable from a normal miss in the pipeline state, so
|
|
193
|
+
* v1 reports it as "miss". The "passthrough" status remains in the type union
|
|
194
|
+
* for forward compatibility.
|
|
195
|
+
*/
|
|
196
|
+
export function deriveCacheStatus(state: {
|
|
197
|
+
cacheHit: boolean;
|
|
198
|
+
cacheSource?: "runtime" | "prerender";
|
|
199
|
+
shouldRevalidate?: boolean;
|
|
200
|
+
}): CacheSegmentStatus {
|
|
201
|
+
if (state.cacheHit) {
|
|
202
|
+
if (state.cacheSource === "prerender") return "prerendered";
|
|
203
|
+
if (state.shouldRevalidate) return "stale";
|
|
204
|
+
return "hit";
|
|
205
|
+
}
|
|
206
|
+
return "miss";
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Build the coarse route-level cache signal array (a single entry keyed by
|
|
211
|
+
* the route key). Used for both the cache.decision telemetry event and the
|
|
212
|
+
* X-Rango-Cache debug header.
|
|
213
|
+
*/
|
|
214
|
+
export function buildCacheSignalSegments(
|
|
215
|
+
routeKey: string,
|
|
216
|
+
state: {
|
|
217
|
+
cacheHit: boolean;
|
|
218
|
+
cacheSource?: "runtime" | "prerender";
|
|
219
|
+
shouldRevalidate?: boolean;
|
|
220
|
+
},
|
|
221
|
+
): CacheSegmentSignal[] {
|
|
222
|
+
return [
|
|
223
|
+
{
|
|
224
|
+
id: routeKey,
|
|
225
|
+
type: "route",
|
|
226
|
+
cacheStatus: deriveCacheStatus(state),
|
|
227
|
+
shouldRevalidate: !!state.shouldRevalidate,
|
|
228
|
+
},
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Serialize cache signal segments into the X-Rango-Cache header value:
|
|
234
|
+
* `<segId>=<status>, <segId2>=<status2>`.
|
|
235
|
+
*/
|
|
236
|
+
export function formatCacheSignalHeader(
|
|
237
|
+
segments: CacheSegmentSignal[],
|
|
238
|
+
): string {
|
|
239
|
+
return segments.map((s) => `${s.id}=${s.cacheStatus}`).join(", ");
|
|
240
|
+
}
|
|
241
|
+
|
|
143
242
|
// ---------------------------------------------------------------------------
|
|
144
243
|
// Sink interface
|
|
145
244
|
// ---------------------------------------------------------------------------
|
package/src/router/types.ts
CHANGED
|
@@ -98,6 +98,14 @@ export interface SegmentResolutionDeps<TEnv = any> {
|
|
|
98
98
|
) => ReactNode | NotFoundBoundaryHandler | null;
|
|
99
99
|
notFoundComponent?: ReactNode | ((props: { pathname: string }) => ReactNode);
|
|
100
100
|
callOnError: (error: unknown, phase: ErrorPhase, context: any) => void;
|
|
101
|
+
/**
|
|
102
|
+
* Router-level default for the per-segment `transition({ viewTransition })`
|
|
103
|
+
* flag, from createRouter({ viewTransition }). Resolved into each segment's
|
|
104
|
+
* transition config during resolution (only `false` is stamped) so the render
|
|
105
|
+
* gate reads the boundary decision off the segment on both server and client.
|
|
106
|
+
* Undefined is treated as "auto" (wrap).
|
|
107
|
+
*/
|
|
108
|
+
viewTransitionDefault?: "auto" | false;
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
/**
|
package/src/router.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { urls } from "./urls.js";
|
|
|
24
24
|
import {
|
|
25
25
|
type EntryData,
|
|
26
26
|
getContext,
|
|
27
|
-
|
|
27
|
+
RangoContext,
|
|
28
28
|
type MetricsStore,
|
|
29
29
|
} from "./server/context";
|
|
30
30
|
import { createHandleStore, type HandleStore } from "./server/handle-store.js";
|
|
@@ -56,6 +56,7 @@ import { buildDebugManifest } from "./router/debug-manifest.js";
|
|
|
56
56
|
|
|
57
57
|
import type { SegmentResolutionDeps, MatchApiDeps } from "./router/types.js";
|
|
58
58
|
import { createHandlerContext } from "./router/handler-context.js";
|
|
59
|
+
import { normalizeBasename } from "./router/basename.js";
|
|
59
60
|
import {
|
|
60
61
|
setupLoaderAccess,
|
|
61
62
|
setupLoaderAccessSilent,
|
|
@@ -90,13 +91,10 @@ import {
|
|
|
90
91
|
RouterRegistry,
|
|
91
92
|
nextRouterAutoId,
|
|
92
93
|
} from "./router/router-registry.js";
|
|
94
|
+
import type { RangoOptions, RootLayoutProps } from "./router/router-options.js";
|
|
93
95
|
import type {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
} from "./router/router-options.js";
|
|
97
|
-
import type {
|
|
98
|
-
RSCRouter,
|
|
99
|
-
RSCRouterInternal,
|
|
96
|
+
Rango,
|
|
97
|
+
RangoInternal,
|
|
100
98
|
RouterRequestInput,
|
|
101
99
|
} from "./router/router-interfaces.js";
|
|
102
100
|
|
|
@@ -115,22 +113,22 @@ import {
|
|
|
115
113
|
// Re-export public types and values from extracted modules
|
|
116
114
|
export { RSC_ROUTER_BRAND, RouterRegistry } from "./router/router-registry.js";
|
|
117
115
|
export type {
|
|
118
|
-
|
|
116
|
+
RangoOptions,
|
|
119
117
|
RootLayoutProps,
|
|
120
118
|
SSRStreamMode,
|
|
121
119
|
SSROptions,
|
|
122
120
|
ResolveStreamingContext,
|
|
123
121
|
} from "./router/router-options.js";
|
|
124
122
|
export type {
|
|
125
|
-
|
|
126
|
-
|
|
123
|
+
Rango,
|
|
124
|
+
RangoInternal,
|
|
127
125
|
RouterRequestInput,
|
|
128
126
|
} from "./router/router-interfaces.js";
|
|
129
127
|
export { toInternal } from "./router/router-interfaces.js";
|
|
130
128
|
|
|
131
129
|
export function createRouter<TEnv = any>(
|
|
132
|
-
options:
|
|
133
|
-
):
|
|
130
|
+
options: RangoOptions<TEnv> = {},
|
|
131
|
+
): Rango<TEnv, {}> {
|
|
134
132
|
const {
|
|
135
133
|
id: userProvidedId,
|
|
136
134
|
$$id: injectedId,
|
|
@@ -158,14 +156,23 @@ export function createRouter<TEnv = any>(
|
|
|
158
156
|
timeouts: timeoutsOption,
|
|
159
157
|
onTimeout,
|
|
160
158
|
originCheck: originCheckOption,
|
|
159
|
+
viewTransition: viewTransitionOption = "auto",
|
|
160
|
+
debugCacheSignal: debugCacheSignalOption = false,
|
|
161
161
|
} = options;
|
|
162
162
|
|
|
163
|
+
// Debug cache signal gate (DEVELOPMENT/TEST ONLY). Enabled by the
|
|
164
|
+
// debugCacheSignal option OR the RANGO_TEST_SIGNALS=1 env flag. When off,
|
|
165
|
+
// no X-Rango-Cache header is emitted and output is byte-identical.
|
|
166
|
+
const cacheSignalEnabled =
|
|
167
|
+
debugCacheSignalOption ||
|
|
168
|
+
(typeof process !== "undefined" &&
|
|
169
|
+
(process as { env?: Record<string, string | undefined> }).env
|
|
170
|
+
?.RANGO_TEST_SIGNALS === "1");
|
|
171
|
+
|
|
163
172
|
// Normalize basename: ensure leading slash, strip trailing slash.
|
|
164
|
-
// A bare "/" is equivalent to no basename.
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
? "/" + basenameOption.replace(/^\/+|\/+$/g, "")
|
|
168
|
-
: undefined;
|
|
173
|
+
// A bare "/" is equivalent to no basename. Shared with the testing
|
|
174
|
+
// primitives via normalizeBasename so they can never drift.
|
|
175
|
+
const basename = normalizeBasename(basenameOption);
|
|
169
176
|
|
|
170
177
|
// Resolve telemetry sink (no-op when not configured)
|
|
171
178
|
const telemetry = resolveSink(telemetrySink);
|
|
@@ -537,6 +544,7 @@ export function createRouter<TEnv = any>(
|
|
|
537
544
|
findNearestNotFoundBoundary,
|
|
538
545
|
notFoundComponent: notFound,
|
|
539
546
|
callOnError,
|
|
547
|
+
viewTransitionDefault: viewTransitionOption,
|
|
540
548
|
};
|
|
541
549
|
|
|
542
550
|
// Match API dependencies
|
|
@@ -664,6 +672,7 @@ export function createRouter<TEnv = any>(
|
|
|
664
672
|
findMatch,
|
|
665
673
|
findInterceptForRoute,
|
|
666
674
|
telemetry: telemetrySink,
|
|
675
|
+
cacheSignalEnabled,
|
|
667
676
|
});
|
|
668
677
|
|
|
669
678
|
const { match, matchPartial, matchError, previewMatch } = matchHandlers;
|
|
@@ -673,7 +682,7 @@ export function createRouter<TEnv = any>(
|
|
|
673
682
|
* The type system tracks accumulated routes through the builder chain
|
|
674
683
|
* Initial TRoutes is {} (empty) to avoid poisoning accumulated types with Record<string, string>
|
|
675
684
|
*/
|
|
676
|
-
const router:
|
|
685
|
+
const router: RangoInternal<TEnv, {}> = {
|
|
677
686
|
__brand: RSC_ROUTER_BRAND,
|
|
678
687
|
id: routerId,
|
|
679
688
|
basename,
|
|
@@ -721,7 +730,7 @@ export function createRouter<TEnv = any>(
|
|
|
721
730
|
};
|
|
722
731
|
|
|
723
732
|
let handlerResult: AllUseItems[] = [];
|
|
724
|
-
|
|
733
|
+
RangoContext.run(
|
|
725
734
|
{
|
|
726
735
|
manifest,
|
|
727
736
|
patterns: routePatterns,
|
|
@@ -999,6 +1008,13 @@ export function createRouter<TEnv = any>(
|
|
|
999
1008
|
// Expose basename for runtime manifest generation
|
|
1000
1009
|
__basename: basename,
|
|
1001
1010
|
|
|
1011
|
+
// Expose router-level boundary defaults for build-time clientChunks
|
|
1012
|
+
// discovery (so a "use client" default boundary lands in app-fallback).
|
|
1013
|
+
// These are createRouter options, never pushed onto EntryData.
|
|
1014
|
+
__defaultErrorBoundary: defaultErrorBoundary,
|
|
1015
|
+
__defaultNotFoundBoundary: defaultNotFoundBoundary,
|
|
1016
|
+
__notFound: notFound,
|
|
1017
|
+
|
|
1002
1018
|
// RSC request handler (lazily created on first call)
|
|
1003
1019
|
fetch: (() => {
|
|
1004
1020
|
// Handler is created on first call and reused
|
|
@@ -1045,9 +1061,9 @@ export function createRouter<TEnv = any>(
|
|
|
1045
1061
|
|
|
1046
1062
|
// If urls option was provided, auto-register them
|
|
1047
1063
|
if (typeof urlsOption === "function") {
|
|
1048
|
-
return router.routes(urlsOption) as
|
|
1064
|
+
return router.routes(urlsOption) as Rango<TEnv, {}>;
|
|
1049
1065
|
} else if (urlsOption) {
|
|
1050
|
-
return router.routes(urlsOption) as
|
|
1066
|
+
return router.routes(urlsOption) as Rango<TEnv, {}>;
|
|
1051
1067
|
}
|
|
1052
1068
|
|
|
1053
1069
|
return router;
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
* RSC rendering) so they can be standalone modules without closure coupling.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type {
|
|
9
|
+
import type { RangoInternal } from "../router/router-interfaces.js";
|
|
10
10
|
import type { ErrorPhase } from "../types.js";
|
|
11
11
|
import type { InvokeOnErrorContext } from "../router/error-handling.js";
|
|
12
12
|
import type { RSCDependencies, LoadSSRModule } from "./types.js";
|
|
13
13
|
import type { SSRStreamMode } from "../router/router-options.js";
|
|
14
14
|
|
|
15
15
|
export interface HandlerContext<TEnv = unknown> {
|
|
16
|
-
router:
|
|
16
|
+
router: RangoInternal<TEnv, any>;
|
|
17
17
|
version: string;
|
|
18
18
|
renderToReadableStream: RSCDependencies["renderToReadableStream"];
|
|
19
19
|
decodeReply: RSCDependencies["decodeReply"];
|
package/src/rsc/handler.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { createElement } from "react";
|
|
11
|
-
import {
|
|
11
|
+
import { isRouteNotFoundError } from "../errors.js";
|
|
12
12
|
import { matchMiddleware, executeMiddleware } from "../router/middleware.js";
|
|
13
13
|
import {
|
|
14
14
|
runWithRequestContext,
|
|
@@ -66,7 +66,10 @@ import {
|
|
|
66
66
|
type ActionContinuation,
|
|
67
67
|
} from "./server-action.js";
|
|
68
68
|
import { handleLoaderFetch } from "./loader-fetch.js";
|
|
69
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
checkRequestOrigin,
|
|
71
|
+
ORIGIN_CHECK_PHASE_BY_MODE,
|
|
72
|
+
} from "./origin-guard.js";
|
|
70
73
|
import { handleRscRendering } from "./rsc-rendering.js";
|
|
71
74
|
import {
|
|
72
75
|
withTimeout,
|
|
@@ -83,6 +86,7 @@ import {
|
|
|
83
86
|
startSSRSetup,
|
|
84
87
|
getSSRSetup,
|
|
85
88
|
mayNeedSSR,
|
|
89
|
+
isRscRequest,
|
|
86
90
|
SSR_SETUP_VAR,
|
|
87
91
|
} from "./ssr-setup.js";
|
|
88
92
|
import {
|
|
@@ -597,10 +601,7 @@ export function createRSCHandler<
|
|
|
597
601
|
routerId: router.id,
|
|
598
602
|
});
|
|
599
603
|
} catch (error) {
|
|
600
|
-
if (
|
|
601
|
-
error instanceof RouteNotFoundError ||
|
|
602
|
-
(error instanceof Error && error.name === "RouteNotFoundError")
|
|
603
|
-
) {
|
|
604
|
+
if (isRouteNotFoundError(error)) {
|
|
604
605
|
// Let the render path handle 404 — match()/matchPartial() will
|
|
605
606
|
// re-throw RouteNotFoundError and the catch block in
|
|
606
607
|
// executeRenderWithMiddleware renders the not-found page.
|
|
@@ -651,14 +652,7 @@ export function createRSCHandler<
|
|
|
651
652
|
}
|
|
652
653
|
|
|
653
654
|
// ---- 3. Origin guard (gate for action/loader/PE modes) ----
|
|
654
|
-
const originPhase
|
|
655
|
-
plan.mode === "action"
|
|
656
|
-
? "action"
|
|
657
|
-
: plan.mode === "loader"
|
|
658
|
-
? "loader"
|
|
659
|
-
: plan.mode === "pe-render"
|
|
660
|
-
? "pe-form"
|
|
661
|
-
: null;
|
|
655
|
+
const originPhase = ORIGIN_CHECK_PHASE_BY_MODE[plan.mode];
|
|
662
656
|
if (originPhase) {
|
|
663
657
|
const originResult = await checkRequestOrigin(
|
|
664
658
|
request,
|
|
@@ -925,47 +919,17 @@ export function createRSCHandler<
|
|
|
925
919
|
);
|
|
926
920
|
}
|
|
927
921
|
|
|
928
|
-
//
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
plan.
|
|
935
|
-
|
|
936
|
-
request,
|
|
937
|
-
env,
|
|
938
|
-
url,
|
|
939
|
-
variables,
|
|
940
|
-
nonce,
|
|
941
|
-
handleStore,
|
|
942
|
-
isPartial,
|
|
943
|
-
);
|
|
944
|
-
}
|
|
945
|
-
|
|
946
|
-
// PE that fell through (handleProgressiveEnhancement returned null)
|
|
947
|
-
// falls back to full render
|
|
948
|
-
if (plan.mode === "pe-render") {
|
|
949
|
-
return executeRenderWithMiddleware(
|
|
950
|
-
plan.route.routeMiddleware,
|
|
951
|
-
false,
|
|
952
|
-
plan.route.routeKey,
|
|
953
|
-
routeReverse,
|
|
954
|
-
request,
|
|
955
|
-
env,
|
|
956
|
-
url,
|
|
957
|
-
variables,
|
|
958
|
-
nonce,
|
|
959
|
-
handleStore,
|
|
960
|
-
false,
|
|
961
|
-
);
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
// Redirect plan that wasn't handled above (full-page redirect — let
|
|
965
|
-
// the pipeline handle it via match() which returns { redirect: url })
|
|
922
|
+
// Full render, partial render, fallen-through PE, and full-page redirect all
|
|
923
|
+
// render through the same middleware-wrapped path. Only full/partial-render
|
|
924
|
+
// carry negotiation + the partial flag; pe/redirect render plainly.
|
|
925
|
+
const isPartial = plan.mode === "partial-render";
|
|
926
|
+
const negotiated =
|
|
927
|
+
plan.mode === "full-render" || plan.mode === "partial-render"
|
|
928
|
+
? plan.negotiated
|
|
929
|
+
: false;
|
|
966
930
|
return executeRenderWithMiddleware(
|
|
967
931
|
plan.route.routeMiddleware,
|
|
968
|
-
|
|
932
|
+
negotiated,
|
|
969
933
|
plan.route.routeKey,
|
|
970
934
|
routeReverse,
|
|
971
935
|
request,
|
|
@@ -974,7 +938,7 @@ export function createRSCHandler<
|
|
|
974
938
|
variables,
|
|
975
939
|
nonce,
|
|
976
940
|
handleStore,
|
|
977
|
-
|
|
941
|
+
isPartial,
|
|
978
942
|
);
|
|
979
943
|
}
|
|
980
944
|
|
|
@@ -1054,10 +1018,7 @@ export function createRSCHandler<
|
|
|
1054
1018
|
}
|
|
1055
1019
|
|
|
1056
1020
|
// Render 404 page for unmatched routes
|
|
1057
|
-
|
|
1058
|
-
error instanceof RouteNotFoundError ||
|
|
1059
|
-
(error instanceof Error && error.name === "RouteNotFoundError");
|
|
1060
|
-
if (isRouteNotFound) {
|
|
1021
|
+
if (isRouteNotFoundError(error)) {
|
|
1061
1022
|
callOnError(error, "routing", {
|
|
1062
1023
|
request,
|
|
1063
1024
|
url,
|
|
@@ -1104,13 +1065,7 @@ export function createRSCHandler<
|
|
|
1104
1065
|
},
|
|
1105
1066
|
});
|
|
1106
1067
|
|
|
1107
|
-
|
|
1108
|
-
isPartial ||
|
|
1109
|
-
(!request.headers.get("accept")?.includes("text/html") &&
|
|
1110
|
-
!url.searchParams.has("__html")) ||
|
|
1111
|
-
url.searchParams.has("__rsc");
|
|
1112
|
-
|
|
1113
|
-
if (isRscRequest) {
|
|
1068
|
+
if (isRscRequest(request, url, isPartial)) {
|
|
1114
1069
|
return createResponseWithMergedHeaders(rscStream, {
|
|
1115
1070
|
status: 404,
|
|
1116
1071
|
headers: { "content-type": "text/x-component;charset=utf-8" },
|
package/src/rsc/helpers.ts
CHANGED
|
@@ -10,7 +10,26 @@ import {
|
|
|
10
10
|
} from "../server/request-context.js";
|
|
11
11
|
import type { RequestContext } from "../server/request-context.js";
|
|
12
12
|
import { resolveLocationStateEntries } from "../browser/react/location-state-shared.js";
|
|
13
|
+
import { isRedirectResponse } from "../response-utils.js";
|
|
13
14
|
import type { MiddlewareEntry, MiddlewareFn } from "../router/middleware.js";
|
|
15
|
+
import { formatCacheSignalHeader } from "../router/telemetry.js";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* DEVELOPMENT/TEST ONLY. When the debug cache signal gate is on,
|
|
19
|
+
* match/matchPartial populate ctx._cacheSignal. Emit it as the X-Rango-Cache
|
|
20
|
+
* header. When the gate is off, ctx._cacheSignal is undefined and NOTHING is
|
|
21
|
+
* attached — output is byte-identical to the default. Header mutation failures
|
|
22
|
+
* are swallowed so immutable Response headers (e.g. protocol-switch) are safe.
|
|
23
|
+
*/
|
|
24
|
+
function applyCacheSignalHeader(target: Headers, ctx: RequestContext): void {
|
|
25
|
+
const signal = ctx._cacheSignal;
|
|
26
|
+
if (!signal || signal.length === 0) return;
|
|
27
|
+
try {
|
|
28
|
+
target.set("X-Rango-Cache", formatCacheSignalHeader(signal));
|
|
29
|
+
} catch {
|
|
30
|
+
// Headers immutable — skip.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
14
33
|
|
|
15
34
|
/**
|
|
16
35
|
* Copy stub headers from the request context onto a target Headers instance:
|
|
@@ -84,6 +103,7 @@ export function createResponseWithMergedHeaders(
|
|
|
84
103
|
const mergedHeaders = new Headers(init.headers);
|
|
85
104
|
applyStubHeaders(mergedHeaders, ctx.res.headers);
|
|
86
105
|
ctx.res.headers.delete("set-cookie");
|
|
106
|
+
applyCacheSignalHeader(mergedHeaders, ctx);
|
|
87
107
|
|
|
88
108
|
// ctx.res.status overrides init.status when explicitly set (e.g. 404 for
|
|
89
109
|
// notFound, 500 for error). Default ctx.res.status is 200.
|
|
@@ -145,10 +165,10 @@ export function interceptRedirectForPartial(
|
|
|
145
165
|
locationState?: Record<string, unknown>,
|
|
146
166
|
) => Response,
|
|
147
167
|
): Response | null {
|
|
148
|
-
|
|
149
|
-
if (!(response.status >= 300 && response.status < 400 && redirectUrl)) {
|
|
168
|
+
if (!isRedirectResponse(response)) {
|
|
150
169
|
return null;
|
|
151
170
|
}
|
|
171
|
+
const redirectUrl = response.headers.get("Location")!;
|
|
152
172
|
const locationState = getLocationState();
|
|
153
173
|
let intercepted: Response;
|
|
154
174
|
if (locationState) {
|
package/src/rsc/index.ts
CHANGED
package/src/rsc/origin-guard.ts
CHANGED
|
@@ -9,11 +9,29 @@
|
|
|
9
9
|
* navigations, bookmarks, and non-browser clients don't send Origin.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
import type { RequestPlan } from "../router/request-classification.js";
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Request phase that triggered the origin check.
|
|
14
16
|
*/
|
|
15
17
|
export type OriginCheckPhase = "action" | "loader" | "pe-form";
|
|
16
18
|
|
|
19
|
+
// Exhaustive over RequestPlan modes so a new mode must be classified here (the
|
|
20
|
+
// security gate) instead of silently falling through to no origin check.
|
|
21
|
+
export const ORIGIN_CHECK_PHASE_BY_MODE: Record<
|
|
22
|
+
RequestPlan["mode"],
|
|
23
|
+
OriginCheckPhase | null
|
|
24
|
+
> = {
|
|
25
|
+
action: "action",
|
|
26
|
+
loader: "loader",
|
|
27
|
+
"pe-render": "pe-form",
|
|
28
|
+
"full-render": null,
|
|
29
|
+
"partial-render": null,
|
|
30
|
+
response: null,
|
|
31
|
+
redirect: null,
|
|
32
|
+
"version-mismatch": null,
|
|
33
|
+
};
|
|
34
|
+
|
|
17
35
|
/**
|
|
18
36
|
* Context passed to the originCheck callback.
|
|
19
37
|
*/
|
|
@@ -116,14 +134,15 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
116
134
|
// Disabled by explicit opt-out
|
|
117
135
|
if (config === false) return null;
|
|
118
136
|
|
|
119
|
-
// Default
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
137
|
+
// Default (true/undefined) becomes a callback returning boolean, so the
|
|
138
|
+
// Response|true|reject resolution below is written once.
|
|
139
|
+
const check: (
|
|
140
|
+
ctx: OriginCheckContext<TEnv>,
|
|
141
|
+
) => boolean | Response | Promise<boolean | Response> =
|
|
142
|
+
config === true || config === undefined
|
|
143
|
+
? () => defaultOriginCheck(request, url)
|
|
144
|
+
: config;
|
|
125
145
|
|
|
126
|
-
// Custom function — build context and call
|
|
127
146
|
const ctx: OriginCheckContext<TEnv> = {
|
|
128
147
|
request,
|
|
129
148
|
url,
|
|
@@ -133,9 +152,8 @@ export async function checkRequestOrigin<TEnv = any>(
|
|
|
133
152
|
defaultCheck: () => defaultOriginCheck(request, url),
|
|
134
153
|
};
|
|
135
154
|
|
|
136
|
-
const result = await
|
|
155
|
+
const result = await check(ctx);
|
|
137
156
|
|
|
138
157
|
if (result instanceof Response) return result;
|
|
139
|
-
|
|
140
|
-
return createForbiddenResponse(request);
|
|
158
|
+
return result === true ? null : createForbiddenResponse(request);
|
|
141
159
|
}
|
|
@@ -11,6 +11,7 @@ import { requireRequestContext } from "../server/request-context.js";
|
|
|
11
11
|
import { contextGet } from "../context-var.js";
|
|
12
12
|
import { NOCACHE_SYMBOL } from "../cache/taint.js";
|
|
13
13
|
import { traverseBack } from "../router/pattern-matching.js";
|
|
14
|
+
import { RESPONSE_TYPE_MIME } from "../router/content-negotiation.js";
|
|
14
15
|
import { createCacheScope } from "../cache/cache-scope.js";
|
|
15
16
|
import { executeMiddleware } from "../router/middleware.js";
|
|
16
17
|
import {
|
|
@@ -121,13 +122,15 @@ export async function handleResponseRoute<TEnv>(
|
|
|
121
122
|
});
|
|
122
123
|
};
|
|
123
124
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
try {
|
|
126
|
+
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
127
|
+
|
|
128
|
+
if (result instanceof Response) {
|
|
129
|
+
return rewrapResponse(result);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Handled before the MIME lookup (json is also a RESPONSE_TYPE_MIME key).
|
|
133
|
+
if (preview.responseType === "json") {
|
|
131
134
|
return createResponseWithMergedHeaders(
|
|
132
135
|
JSON.stringify({ data: result }),
|
|
133
136
|
{
|
|
@@ -135,10 +138,28 @@ export async function handleResponseRoute<TEnv>(
|
|
|
135
138
|
headers: { "content-type": "application/json;charset=utf-8" },
|
|
136
139
|
},
|
|
137
140
|
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Object.hasOwn (not truthiness) so prototype names like "toString" are not
|
|
144
|
+
// matched; image/stream/any are absent and fall through to the throw.
|
|
145
|
+
if (Object.hasOwn(RESPONSE_TYPE_MIME, preview.responseType)) {
|
|
146
|
+
return createResponseWithMergedHeaders(String(result), {
|
|
147
|
+
status: 200,
|
|
148
|
+
headers: {
|
|
149
|
+
"content-type": `${RESPONSE_TYPE_MIME[preview.responseType]};charset=utf-8`,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new Error(
|
|
155
|
+
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
156
|
+
);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
159
|
+
const isDev = process.env.NODE_ENV !== "production";
|
|
160
|
+
const status = error instanceof RouterError ? error.status : 500;
|
|
161
|
+
|
|
162
|
+
if (preview.responseType === "json") {
|
|
142
163
|
return createResponseWithMergedHeaders(
|
|
143
164
|
JSON.stringify({
|
|
144
165
|
error: createResponseErrorPayload(error, isDev),
|
|
@@ -149,48 +170,7 @@ export async function handleResponseRoute<TEnv>(
|
|
|
149
170
|
},
|
|
150
171
|
);
|
|
151
172
|
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Non-JSON response routes: catch errors and return plain Response
|
|
155
|
-
try {
|
|
156
|
-
const result = await (preview.handler as Function)(responseHandlerCtx);
|
|
157
|
-
|
|
158
|
-
if (result instanceof Response) {
|
|
159
|
-
return rewrapResponse(result);
|
|
160
|
-
}
|
|
161
173
|
|
|
162
|
-
// Auto-wrap based on response type tag
|
|
163
|
-
switch (preview.responseType) {
|
|
164
|
-
case "text":
|
|
165
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
166
|
-
status: 200,
|
|
167
|
-
headers: { "content-type": "text/plain;charset=utf-8" },
|
|
168
|
-
});
|
|
169
|
-
case "html":
|
|
170
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
171
|
-
status: 200,
|
|
172
|
-
headers: { "content-type": "text/html;charset=utf-8" },
|
|
173
|
-
});
|
|
174
|
-
case "xml":
|
|
175
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
176
|
-
status: 200,
|
|
177
|
-
headers: { "content-type": "application/xml;charset=utf-8" },
|
|
178
|
-
});
|
|
179
|
-
case "md":
|
|
180
|
-
return createResponseWithMergedHeaders(String(result), {
|
|
181
|
-
status: 200,
|
|
182
|
-
headers: { "content-type": "text/markdown;charset=utf-8" },
|
|
183
|
-
});
|
|
184
|
-
default:
|
|
185
|
-
// image, stream, any -- must return Response
|
|
186
|
-
throw new Error(
|
|
187
|
-
`Response route handler for "${preview.responseType}" must return a Response object, got ${typeof result}`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
} catch (error) {
|
|
191
|
-
handlerCtx.callOnError(error, "handler", errorCtx);
|
|
192
|
-
const isDev = process.env.NODE_ENV !== "production";
|
|
193
|
-
const status = error instanceof RouterError ? error.status : 500;
|
|
194
174
|
const message =
|
|
195
175
|
error instanceof RouterError
|
|
196
176
|
? error.message
|