@rangojs/router 0.0.0-experimental.18 → 0.0.0-experimental.19
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 +46 -8
- package/dist/bin/rango.js +105 -18
- package/dist/vite/index.js +227 -93
- package/package.json +15 -14
- package/skills/hooks/SKILL.md +1 -1
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +94 -1
- package/skills/middleware/SKILL.md +81 -0
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +187 -17
- package/skills/route/SKILL.md +42 -1
- package/skills/router-setup/SKILL.md +77 -0
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +38 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +25 -27
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +0 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +46 -13
- package/src/browser/navigation-client.ts +32 -61
- package/src/browser/navigation-store.ts +1 -31
- package/src/browser/navigation-transaction.ts +46 -207
- package/src/browser/partial-update.ts +102 -150
- package/src/browser/{prefetch-cache.ts → prefetch/cache.ts} +23 -4
- package/src/browser/{prefetch-fetch.ts → prefetch/fetch.ts} +36 -8
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/{prefetch-queue.ts → prefetch/queue.ts} +10 -3
- package/src/browser/react/Link.tsx +28 -23
- package/src/browser/react/NavigationProvider.tsx +9 -1
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +1 -1
- package/src/browser/react/location-state.ts +2 -0
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/use-action.ts +9 -1
- package/src/browser/react/use-handle.ts +3 -25
- package/src/browser/react/use-params.ts +2 -4
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +1 -1
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +7 -60
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +29 -23
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +115 -96
- package/src/browser/types.ts +1 -31
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +5 -0
- package/src/build/generate-route-types.ts +2 -0
- package/src/build/route-types/codegen.ts +13 -4
- package/src/build/route-types/include-resolution.ts +13 -0
- package/src/build/route-types/per-module-writer.ts +15 -3
- package/src/build/route-types/router-processing.ts +45 -3
- package/src/build/runtime-discovery.ts +13 -1
- 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 +132 -96
- package/src/cache/cache-scope.ts +71 -73
- package/src/cache/cf/cf-cache-store.ts +9 -4
- package/src/cache/document-cache.ts +72 -47
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/memory-segment-store.ts +18 -7
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +101 -112
- package/src/cache/taint.ts +26 -0
- package/src/client.tsx +53 -30
- package/src/errors.ts +6 -1
- package/src/handle.ts +1 -1
- package/src/handles/MetaTags.tsx +5 -2
- package/src/host/cookie-handler.ts +8 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +33 -1
- package/src/index.ts +27 -0
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +4 -3
- package/src/prerender.ts +76 -18
- package/src/reverse.ts +11 -7
- package/src/root-error-boundary.tsx +30 -26
- package/src/route-definition/dsl-helpers.ts +9 -6
- package/src/route-definition/redirect.ts +15 -3
- package/src/route-map-builder.ts +38 -2
- package/src/route-name.ts +53 -0
- package/src/route-types.ts +7 -0
- package/src/router/content-negotiation.ts +1 -1
- package/src/router/debug-manifest.ts +16 -3
- package/src/router/handler-context.ts +94 -15
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +1 -0
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +61 -7
- package/src/router/match-context.ts +3 -0
- package/src/router/match-handlers.ts +185 -11
- package/src/router/match-middleware/background-revalidation.ts +65 -85
- package/src/router/match-middleware/cache-lookup.ts +69 -4
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/middleware-types.ts +7 -0
- package/src/router/middleware.ts +93 -8
- package/src/router/pattern-matching.ts +41 -5
- package/src/router/prerender-match.ts +34 -6
- package/src/router/preview-match.ts +7 -1
- package/src/router/revalidation.ts +61 -2
- package/src/router/router-context.ts +15 -0
- package/src/router/router-interfaces.ts +34 -0
- package/src/router/router-options.ts +200 -0
- package/src/router/segment-resolution/fresh.ts +123 -30
- package/src/router/segment-resolution/helpers.ts +19 -0
- package/src/router/segment-resolution/loader-cache.ts +37 -146
- package/src/router/segment-resolution/revalidation.ts +358 -94
- package/src/router/segment-wrappers.ts +3 -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/types.ts +7 -1
- package/src/router.ts +155 -11
- package/src/rsc/handler-context.ts +11 -0
- package/src/rsc/handler.ts +380 -88
- package/src/rsc/helpers.ts +25 -16
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +232 -19
- package/src/rsc/response-route-handler.ts +37 -26
- package/src/rsc/rsc-rendering.ts +12 -5
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +134 -58
- package/src/rsc/types.ts +8 -0
- package/src/search-params.ts +22 -10
- package/src/server/context.ts +53 -5
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +66 -9
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +90 -9
- package/src/ssr/index.tsx +63 -27
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +1 -6
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +5 -0
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +9 -0
- package/src/types/handler-context.ts +35 -13
- package/src/types/loader-types.ts +7 -0
- package/src/types/route-entry.ts +28 -0
- package/src/urls/include-helper.ts +49 -8
- package/src/urls/index.ts +1 -0
- package/src/urls/path-helper-types.ts +30 -12
- package/src/urls/path-helper.ts +17 -2
- package/src/urls/pattern-types.ts +21 -1
- package/src/urls/response-types.ts +27 -2
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +12 -4
- package/src/vite/discovery/bundle-postprocess.ts +12 -7
- package/src/vite/discovery/discover-routers.ts +30 -18
- package/src/vite/discovery/prerender-collection.ts +24 -27
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/rango.ts +3 -3
- package/src/vite/router-discovery.ts +99 -36
- package/src/vite/utils/prerender-utils.ts +21 -0
- package/src/vite/utils/shared-utils.ts +3 -1
- package/src/browser/request-controller.ts +0 -164
- package/src/href-context.ts +0 -33
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/src/browser/{prefetch-observer.ts → prefetch/observer.ts} +0 -0
|
@@ -142,79 +142,76 @@ export const deserializeComponent: (encoded: string) => Promise<unknown> =
|
|
|
142
142
|
export async function serializeSegments(
|
|
143
143
|
segments: ResolvedSegment[],
|
|
144
144
|
): Promise<SerializedSegmentData[]> {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const encodedLoaderDataPromise = await rscSerialize(
|
|
191
|
-
loaderDataPromiseResolved,
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
serialized.push({
|
|
195
|
-
encoded,
|
|
196
|
-
encodedLayout,
|
|
197
|
-
encodedLoading,
|
|
198
|
-
encodedLoaderData,
|
|
199
|
-
encodedLoaderDataPromise,
|
|
200
|
-
metadata: {
|
|
201
|
-
id: segment.id,
|
|
202
|
-
type: segment.type,
|
|
203
|
-
namespace: segment.namespace,
|
|
204
|
-
index: segment.index,
|
|
205
|
-
params: segment.params,
|
|
206
|
-
slot: segment.slot,
|
|
207
|
-
belongsToRoute: segment.belongsToRoute,
|
|
208
|
-
layoutName: segment.layoutName,
|
|
209
|
-
parallelName: segment.parallelName,
|
|
210
|
-
loaderId: segment.loaderId,
|
|
211
|
-
loaderIds: segment.loaderIds,
|
|
212
|
-
transition: segment.transition,
|
|
213
|
-
},
|
|
214
|
-
});
|
|
215
|
-
}
|
|
145
|
+
return Promise.all(
|
|
146
|
+
segments.map(async (segment): Promise<SerializedSegmentData> => {
|
|
147
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
148
|
+
|
|
149
|
+
// Await component if it's a Promise (intercepts with loading keep component as Promise)
|
|
150
|
+
const componentResolved =
|
|
151
|
+
segment.component instanceof Promise
|
|
152
|
+
? await segment.component
|
|
153
|
+
: segment.component;
|
|
154
|
+
|
|
155
|
+
// Serialize the component to RSC stream
|
|
156
|
+
const stream = renderToReadableStream(componentResolved, {
|
|
157
|
+
temporaryReferences,
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// RSC-serialize loading: "null" string distinguishes explicit null from undefined
|
|
161
|
+
const encodedLoading =
|
|
162
|
+
segment.loading !== undefined
|
|
163
|
+
? segment.loading === null
|
|
164
|
+
? "null"
|
|
165
|
+
: await rscSerialize(segment.loading)
|
|
166
|
+
: undefined;
|
|
167
|
+
|
|
168
|
+
// Await loaderData / loaderDataPromise if they're Promises
|
|
169
|
+
const loaderDataResolved =
|
|
170
|
+
segment.loaderData instanceof Promise
|
|
171
|
+
? await segment.loaderData
|
|
172
|
+
: segment.loaderData;
|
|
173
|
+
const loaderDataPromiseResolved =
|
|
174
|
+
segment.loaderDataPromise instanceof Promise
|
|
175
|
+
? await segment.loaderDataPromise
|
|
176
|
+
: segment.loaderDataPromise;
|
|
177
|
+
|
|
178
|
+
// Parallelize stream-to-string and RSC serialization of sub-fields
|
|
179
|
+
const [
|
|
180
|
+
encoded,
|
|
181
|
+
encodedLayout,
|
|
182
|
+
encodedLoaderData,
|
|
183
|
+
encodedLoaderDataPromise,
|
|
184
|
+
] = await Promise.all([
|
|
185
|
+
streamToString(stream),
|
|
186
|
+
segment.layout ? rscSerialize(segment.layout) : undefined,
|
|
187
|
+
rscSerialize(loaderDataResolved),
|
|
188
|
+
rscSerialize(loaderDataPromiseResolved),
|
|
189
|
+
]);
|
|
216
190
|
|
|
217
|
-
|
|
191
|
+
return {
|
|
192
|
+
encoded,
|
|
193
|
+
encodedLayout,
|
|
194
|
+
encodedLoading,
|
|
195
|
+
encodedLoaderData,
|
|
196
|
+
encodedLoaderDataPromise,
|
|
197
|
+
metadata: {
|
|
198
|
+
id: segment.id,
|
|
199
|
+
type: segment.type,
|
|
200
|
+
namespace: segment.namespace,
|
|
201
|
+
index: segment.index,
|
|
202
|
+
params: segment.params,
|
|
203
|
+
slot: segment.slot,
|
|
204
|
+
belongsToRoute: segment.belongsToRoute,
|
|
205
|
+
layoutName: segment.layoutName,
|
|
206
|
+
parallelName: segment.parallelName,
|
|
207
|
+
loaderId: segment.loaderId,
|
|
208
|
+
loaderIds: segment.loaderIds,
|
|
209
|
+
transition: segment.transition,
|
|
210
|
+
mountPath: segment.mountPath,
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}),
|
|
214
|
+
);
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
/**
|
|
@@ -224,44 +221,36 @@ export async function serializeSegments(
|
|
|
224
221
|
export async function deserializeSegments(
|
|
225
222
|
data: SerializedSegmentData[],
|
|
226
223
|
): Promise<ResolvedSegment[]> {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
layout,
|
|
260
|
-
loading: loadingData,
|
|
261
|
-
loaderData,
|
|
262
|
-
loaderDataPromise,
|
|
263
|
-
} as ResolvedSegment);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return segments;
|
|
224
|
+
return Promise.all(
|
|
225
|
+
data.map(async (item): Promise<ResolvedSegment> => {
|
|
226
|
+
const temporaryReferences = createTemporaryReferenceSet();
|
|
227
|
+
|
|
228
|
+
// Handle the "null" sentinel for loading before RSC deserialization.
|
|
229
|
+
// During serialization, loading: null is stored as the string "null" to
|
|
230
|
+
// distinguish it from undefined.
|
|
231
|
+
const loadingIsNullSentinel = item.encodedLoading === "null";
|
|
232
|
+
|
|
233
|
+
const [component, layout, loaderData, loaderDataPromise, loadingData] =
|
|
234
|
+
await Promise.all([
|
|
235
|
+
createFromReadableStream(stringToStream(item.encoded), {
|
|
236
|
+
temporaryReferences,
|
|
237
|
+
}),
|
|
238
|
+
rscDeserialize(item.encodedLayout),
|
|
239
|
+
rscDeserialize(item.encodedLoaderData),
|
|
240
|
+
rscDeserialize(item.encodedLoaderDataPromise),
|
|
241
|
+
loadingIsNullSentinel
|
|
242
|
+
? (null as any)
|
|
243
|
+
: rscDeserialize(item.encodedLoading),
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
...item.metadata,
|
|
248
|
+
component,
|
|
249
|
+
layout,
|
|
250
|
+
loading: loadingData,
|
|
251
|
+
loaderData,
|
|
252
|
+
loaderDataPromise,
|
|
253
|
+
} as ResolvedSegment;
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
267
256
|
}
|
package/src/cache/taint.ts
CHANGED
|
@@ -25,11 +25,37 @@ export function isTainted(value: unknown): boolean {
|
|
|
25
25
|
* cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
|
|
26
26
|
* throw if present — reads would cache per-request data under a shared key,
|
|
27
27
|
* and side effects would be lost on cache hit.
|
|
28
|
+
*
|
|
29
|
+
* The value is a numeric reference count, not a boolean. Multiple concurrent
|
|
30
|
+
* cached functions sharing the same ctx/requestCtx each increment on entry
|
|
31
|
+
* and decrement on exit. Guards fire when count > 0.
|
|
28
32
|
*/
|
|
29
33
|
export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
|
|
30
34
|
"rango:inside-cache-exec",
|
|
31
35
|
) as any;
|
|
32
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Increment the INSIDE_CACHE_EXEC ref count on an object.
|
|
39
|
+
*/
|
|
40
|
+
export function stampCacheExec(obj: object): void {
|
|
41
|
+
const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
|
|
42
|
+
(obj as any)[INSIDE_CACHE_EXEC] = current + 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Decrement the INSIDE_CACHE_EXEC ref count on an object.
|
|
47
|
+
* Deletes the symbol when the count reaches zero so the `in` check
|
|
48
|
+
* used by guards no longer fires.
|
|
49
|
+
*/
|
|
50
|
+
export function unstampCacheExec(obj: object): void {
|
|
51
|
+
const current = (obj as any)[INSIDE_CACHE_EXEC] ?? 0;
|
|
52
|
+
if (current <= 1) {
|
|
53
|
+
delete (obj as any)[INSIDE_CACHE_EXEC];
|
|
54
|
+
} else {
|
|
55
|
+
(obj as any)[INSIDE_CACHE_EXEC] = current - 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
33
59
|
/**
|
|
34
60
|
* Throw if ctx is inside a "use cache" execution.
|
|
35
61
|
* Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
|
package/src/client.tsx
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
LoaderBoundary,
|
|
22
22
|
} from "./route-content-wrapper.js";
|
|
23
23
|
import { OutletProvider } from "./outlet-provider.js";
|
|
24
|
+
import { MountContextProvider } from "./browser/react/mount-context.js";
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Outlet component - renders child content in layouts
|
|
@@ -87,6 +88,8 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
87
88
|
content = segment.component ?? null;
|
|
88
89
|
}
|
|
89
90
|
|
|
91
|
+
let result: ReactNode;
|
|
92
|
+
|
|
90
93
|
// If segment has a layout, wrap appropriately
|
|
91
94
|
if (segment.layout) {
|
|
92
95
|
// Check if this segment has loaders that need streaming
|
|
@@ -106,25 +109,23 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
106
109
|
</LoaderBoundary>
|
|
107
110
|
);
|
|
108
111
|
|
|
109
|
-
|
|
112
|
+
result = (
|
|
110
113
|
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
111
114
|
{segment.layout}
|
|
112
115
|
</OutletProvider>
|
|
113
116
|
);
|
|
117
|
+
} else {
|
|
118
|
+
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
119
|
+
result = (
|
|
120
|
+
<OutletProvider content={content} segment={segment}>
|
|
121
|
+
{segment.layout}
|
|
122
|
+
</OutletProvider>
|
|
123
|
+
);
|
|
114
124
|
}
|
|
115
|
-
|
|
116
|
-
// No loaders - wrap
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
{segment.layout}
|
|
120
|
-
</OutletProvider>
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
125
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
126
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
127
|
-
return (
|
|
125
|
+
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
126
|
+
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
127
|
+
// This is common for intercept routes that use useLoader without a custom layout
|
|
128
|
+
result = (
|
|
128
129
|
<LoaderBoundary
|
|
129
130
|
loaderDataPromise={segment.loaderDataPromise}
|
|
130
131
|
loaderIds={segment.loaderIds}
|
|
@@ -136,9 +137,20 @@ export function Outlet({ name }: { name?: `@${string}` } = {}): ReactNode {
|
|
|
136
137
|
{content}
|
|
137
138
|
</LoaderBoundary>
|
|
138
139
|
);
|
|
140
|
+
} else {
|
|
141
|
+
result = content;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
|
-
|
|
144
|
+
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
145
|
+
if (segment.mountPath) {
|
|
146
|
+
return (
|
|
147
|
+
<MountContextProvider value={segment.mountPath}>
|
|
148
|
+
{result}
|
|
149
|
+
</MountContextProvider>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return result;
|
|
142
154
|
}
|
|
143
155
|
|
|
144
156
|
// Default: render child content
|
|
@@ -202,6 +214,8 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
|
202
214
|
content = segment.component ?? null;
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
let result: ReactNode;
|
|
218
|
+
|
|
205
219
|
// If segment has a layout, wrap appropriately
|
|
206
220
|
if (segment.layout) {
|
|
207
221
|
// Check if this segment has loaders that need streaming
|
|
@@ -220,25 +234,23 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
|
220
234
|
</LoaderBoundary>
|
|
221
235
|
);
|
|
222
236
|
|
|
223
|
-
|
|
237
|
+
result = (
|
|
224
238
|
<OutletProvider content={loaderAwareContent} segment={segment}>
|
|
225
239
|
{segment.layout}
|
|
226
240
|
</OutletProvider>
|
|
227
241
|
);
|
|
242
|
+
} else {
|
|
243
|
+
// No loaders - wrap in OutletProvider so layout can use <Outlet />
|
|
244
|
+
result = (
|
|
245
|
+
<OutletProvider content={content} segment={segment}>
|
|
246
|
+
{segment.layout}
|
|
247
|
+
</OutletProvider>
|
|
248
|
+
);
|
|
228
249
|
}
|
|
229
|
-
|
|
230
|
-
// No loaders - wrap
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
{segment.layout}
|
|
234
|
-
</OutletProvider>
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
239
|
-
// This is common for intercept routes that use useLoader without a custom layout
|
|
240
|
-
if (segment.loaderDataPromise && segment.loaderIds) {
|
|
241
|
-
return (
|
|
250
|
+
} else if (segment.loaderDataPromise && segment.loaderIds) {
|
|
251
|
+
// No layout but has loaders - wrap content with LoaderBoundary for useLoader context
|
|
252
|
+
// This is common for intercept routes that use useLoader without a custom layout
|
|
253
|
+
result = (
|
|
242
254
|
<LoaderBoundary
|
|
243
255
|
loaderDataPromise={segment.loaderDataPromise}
|
|
244
256
|
loaderIds={segment.loaderIds}
|
|
@@ -250,9 +262,20 @@ export function ParallelOutlet({ name }: { name: `@${string}` }): ReactNode {
|
|
|
250
262
|
{content}
|
|
251
263
|
</LoaderBoundary>
|
|
252
264
|
);
|
|
265
|
+
} else {
|
|
266
|
+
result = content;
|
|
253
267
|
}
|
|
254
268
|
|
|
255
|
-
|
|
269
|
+
// Wrap with MountContextProvider for include() scoped parallel/intercept slots
|
|
270
|
+
if (segment.mountPath) {
|
|
271
|
+
return (
|
|
272
|
+
<MountContextProvider value={segment.mountPath}>
|
|
273
|
+
{result}
|
|
274
|
+
</MountContextProvider>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return result;
|
|
256
279
|
}
|
|
257
280
|
|
|
258
281
|
// OutletProvider is defined in outlet-provider.tsx to break a circular
|
package/src/errors.ts
CHANGED
|
@@ -327,7 +327,12 @@ export function sanitizeError(error: unknown): Response {
|
|
|
327
327
|
return error;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
|
|
330
|
+
// Vite replaces import.meta.env.DEV at compile time. The fallback covers
|
|
331
|
+
// non-Vite environments (plain Node, test runners without Vite transforms).
|
|
332
|
+
// SECURITY: fail closed — default to production when the environment is ambiguous.
|
|
333
|
+
const isDev =
|
|
334
|
+
(import.meta as any).env?.DEV ??
|
|
335
|
+
globalThis.process?.env?.NODE_ENV === "development";
|
|
331
336
|
|
|
332
337
|
if (isDev) {
|
|
333
338
|
// Development: Send full error details for debugging
|
package/src/handle.ts
CHANGED
|
@@ -95,7 +95,7 @@ export function createHandle<TData, TAccumulated = TData[]>(
|
|
|
95
95
|
): Handle<TData, TAccumulated> {
|
|
96
96
|
const handleId = __injectedId ?? "";
|
|
97
97
|
|
|
98
|
-
if (!handleId && process.env.NODE_ENV
|
|
98
|
+
if (!handleId && process.env.NODE_ENV === "development") {
|
|
99
99
|
throw new Error(
|
|
100
100
|
"[rsc-router] Handle is missing $$id. " +
|
|
101
101
|
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
package/src/handles/MetaTags.tsx
CHANGED
|
@@ -28,8 +28,9 @@ import { use } from "react";
|
|
|
28
28
|
import { useHandle } from "../browser/react/use-handle.js";
|
|
29
29
|
import { Meta } from "./meta.js";
|
|
30
30
|
import type { MetaDescriptor, MetaDescriptorBase } from "../router/types.js";
|
|
31
|
-
import {
|
|
31
|
+
import { useThemeContext } from "../theme/theme-context.js";
|
|
32
32
|
import { generateThemeScript } from "../theme/theme-script.js";
|
|
33
|
+
import { useNonce } from "../browser/react/nonce-context.js";
|
|
33
34
|
|
|
34
35
|
// Type guards for MetaDescriptorBase variants
|
|
35
36
|
function hasCharSet(d: MetaDescriptorBase): d is { charSet: "utf-8" } {
|
|
@@ -216,13 +217,15 @@ function AsyncMetaTag({
|
|
|
216
217
|
*/
|
|
217
218
|
export function MetaTags(): React.ReactNode {
|
|
218
219
|
const descriptors = useHandle(Meta) as MetaDescriptor[];
|
|
219
|
-
const themeConfig =
|
|
220
|
+
const themeConfig = useThemeContext()?.config ?? null;
|
|
221
|
+
const nonce = useNonce();
|
|
220
222
|
|
|
221
223
|
return (
|
|
222
224
|
<>
|
|
223
225
|
{/* Theme script must be first to prevent FOUC */}
|
|
224
226
|
{themeConfig && (
|
|
225
227
|
<script
|
|
228
|
+
nonce={nonce}
|
|
226
229
|
dangerouslySetInnerHTML={{ __html: generateThemeScript(themeConfig) }}
|
|
227
230
|
/>
|
|
228
231
|
)}
|
|
@@ -26,9 +26,14 @@ export function parseCookies(request: Request): Record<string, string> {
|
|
|
26
26
|
const pairs = cookieHeader.split(";");
|
|
27
27
|
|
|
28
28
|
for (const pair of pairs) {
|
|
29
|
-
const [
|
|
30
|
-
if (
|
|
31
|
-
|
|
29
|
+
const [name, ...rest] = pair.trim().split("=");
|
|
30
|
+
if (name && rest.length > 0) {
|
|
31
|
+
const value = rest.join("=");
|
|
32
|
+
try {
|
|
33
|
+
cookies[name] = decodeURIComponent(value);
|
|
34
|
+
} catch {
|
|
35
|
+
cookies[name] = value;
|
|
36
|
+
}
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
package/src/host/router.ts
CHANGED
|
@@ -149,7 +149,20 @@ export function createHostRouter(options: HostRouterOptions = {}): HostRouter {
|
|
|
149
149
|
return finalHandler();
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
|
|
152
|
+
// Guard against double next() calls — a second call would
|
|
153
|
+
// re-enter the downstream chain and run handlers/side-effects twice.
|
|
154
|
+
let nextCalled = false;
|
|
155
|
+
const guardedNext = (): Promise<Response> => {
|
|
156
|
+
if (nextCalled) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`[HostRouter] Middleware called next() more than once.`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
nextCalled = true;
|
|
162
|
+
return next();
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return mw(request, input, guardedNext);
|
|
153
166
|
}
|
|
154
167
|
|
|
155
168
|
return next();
|
package/src/href-client.ts
CHANGED
|
@@ -182,7 +182,9 @@ export type ValidPaths<TRoutes = GetRegisteredRoutes> =
|
|
|
182
182
|
*/
|
|
183
183
|
export function href<T extends ValidPaths>(path: T, mount?: string): string {
|
|
184
184
|
if (mount && mount !== "/") {
|
|
185
|
-
|
|
185
|
+
// Strip trailing slash from mount to avoid double-slash when joining
|
|
186
|
+
const normalizedMount = mount.endsWith("/") ? mount.slice(0, -1) : mount;
|
|
187
|
+
return normalizedMount + path;
|
|
186
188
|
}
|
|
187
189
|
return path;
|
|
188
190
|
}
|
package/src/index.rsc.ts
CHANGED
|
@@ -72,7 +72,12 @@ export type {
|
|
|
72
72
|
} from "./types.js";
|
|
73
73
|
|
|
74
74
|
// Router options type (server-only, so import directly)
|
|
75
|
-
export type {
|
|
75
|
+
export type {
|
|
76
|
+
RSCRouterOptions,
|
|
77
|
+
SSRStreamMode,
|
|
78
|
+
SSROptions,
|
|
79
|
+
ResolveStreamingContext,
|
|
80
|
+
} from "./router.js";
|
|
76
81
|
|
|
77
82
|
// Server-side createLoader and redirect
|
|
78
83
|
export {
|
|
@@ -230,3 +235,30 @@ export {
|
|
|
230
235
|
|
|
231
236
|
// Path-based response type lookup from RegisteredRoutes
|
|
232
237
|
export type { PathResponse } from "./href-client.js";
|
|
238
|
+
|
|
239
|
+
// Telemetry sink
|
|
240
|
+
export { createConsoleSink } from "./router/telemetry.js";
|
|
241
|
+
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
242
|
+
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
243
|
+
export type {
|
|
244
|
+
TelemetrySink,
|
|
245
|
+
TelemetryEvent,
|
|
246
|
+
RequestStartEvent,
|
|
247
|
+
RequestEndEvent,
|
|
248
|
+
RequestErrorEvent,
|
|
249
|
+
RequestTimeoutEvent,
|
|
250
|
+
LoaderStartEvent,
|
|
251
|
+
LoaderEndEvent,
|
|
252
|
+
LoaderErrorEvent,
|
|
253
|
+
HandlerErrorEvent,
|
|
254
|
+
CacheDecisionEvent,
|
|
255
|
+
RevalidationDecisionEvent,
|
|
256
|
+
} from "./router/telemetry.js";
|
|
257
|
+
|
|
258
|
+
// Timeout types and error class
|
|
259
|
+
export { RouterTimeoutError } from "./router/timeout.js";
|
|
260
|
+
export type {
|
|
261
|
+
RouterTimeouts,
|
|
262
|
+
TimeoutPhase,
|
|
263
|
+
TimeoutContext,
|
|
264
|
+
} from "./router/timeout.js";
|
package/src/index.ts
CHANGED
|
@@ -282,3 +282,30 @@ export {
|
|
|
282
282
|
|
|
283
283
|
// Path-based response type lookup from RegisteredRoutes
|
|
284
284
|
export type { PathResponse } from "./href-client.js";
|
|
285
|
+
|
|
286
|
+
// Telemetry sink
|
|
287
|
+
export { createConsoleSink } from "./router/telemetry.js";
|
|
288
|
+
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
289
|
+
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
290
|
+
export type {
|
|
291
|
+
TelemetrySink,
|
|
292
|
+
TelemetryEvent,
|
|
293
|
+
RequestStartEvent,
|
|
294
|
+
RequestEndEvent,
|
|
295
|
+
RequestErrorEvent,
|
|
296
|
+
RequestTimeoutEvent,
|
|
297
|
+
LoaderStartEvent,
|
|
298
|
+
LoaderEndEvent,
|
|
299
|
+
LoaderErrorEvent,
|
|
300
|
+
HandlerErrorEvent,
|
|
301
|
+
CacheDecisionEvent,
|
|
302
|
+
RevalidationDecisionEvent,
|
|
303
|
+
} from "./router/telemetry.js";
|
|
304
|
+
|
|
305
|
+
// Timeout types and error class
|
|
306
|
+
export { RouterTimeoutError } from "./router/timeout.js";
|
|
307
|
+
export type {
|
|
308
|
+
RouterTimeouts,
|
|
309
|
+
TimeoutPhase,
|
|
310
|
+
TimeoutContext,
|
|
311
|
+
} from "./router/timeout.js";
|
package/src/loader.rsc.ts
CHANGED
|
@@ -52,11 +52,19 @@ export function createLoader<T>(
|
|
|
52
52
|
// For fetchable loaders, __injectedId is also passed as a parameter
|
|
53
53
|
const loaderId = __injectedId || "";
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
if (!loaderId && process.env.NODE_ENV === "development") {
|
|
56
|
+
throw new Error(
|
|
57
|
+
"[rsc-router] Loader is missing $$id. " +
|
|
58
|
+
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
59
|
+
"the loader is exported with: export const MyLoader = createLoader(...)",
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// If not fetchable, store fn in registry (for SSR ctx.use() resolution)
|
|
64
|
+
// but mark fetchable=false so the _rsc_loader endpoint rejects it.
|
|
57
65
|
if (fetchable === undefined) {
|
|
58
66
|
if (fn && loaderId) {
|
|
59
|
-
registerFetchableLoader(loaderId, fn, []);
|
|
67
|
+
registerFetchableLoader(loaderId, fn, [], false);
|
|
60
68
|
}
|
|
61
69
|
return {
|
|
62
70
|
__brand: "loader",
|
|
@@ -71,7 +79,7 @@ export function createLoader<T>(
|
|
|
71
79
|
// Register the function in the internal registry by $$id (server-side only)
|
|
72
80
|
// The loader fetch handler looks it up by $$id when load() is called from the client.
|
|
73
81
|
if (fn && loaderId) {
|
|
74
|
-
registerFetchableLoader(loaderId, fn, middleware);
|
|
82
|
+
registerFetchableLoader(loaderId, fn, middleware, true);
|
|
75
83
|
}
|
|
76
84
|
|
|
77
85
|
return {
|
package/src/loader.ts
CHANGED
|
@@ -49,6 +49,14 @@ export function createLoader<T>(
|
|
|
49
49
|
): LoaderDefinition<Awaited<T>, Record<string, string | undefined>> {
|
|
50
50
|
const loaderId = __injectedId || "";
|
|
51
51
|
|
|
52
|
+
if (!loaderId && process.env.NODE_ENV === "development") {
|
|
53
|
+
throw new Error(
|
|
54
|
+
"[rsc-router] Loader is missing $$id. " +
|
|
55
|
+
"Make sure the exposeInternalIds Vite plugin is enabled and " +
|
|
56
|
+
"the loader is exported with: export const MyLoader = createLoader(...)",
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
52
60
|
return {
|
|
53
61
|
__brand: "loader",
|
|
54
62
|
$$id: loaderId,
|
package/src/prerender/store.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface PrerenderStore {
|
|
|
21
21
|
get(
|
|
22
22
|
routeName: string,
|
|
23
23
|
paramHash: string,
|
|
24
|
-
meta?: { pathname: string },
|
|
24
|
+
meta?: { pathname: string; isPassthroughRoute?: boolean },
|
|
25
25
|
): PrerenderEntry | null | Promise<PrerenderEntry | null>;
|
|
26
26
|
}
|
|
27
27
|
|
|
@@ -58,11 +58,12 @@ declare global {
|
|
|
58
58
|
*/
|
|
59
59
|
export function createDevPrerenderStore(devUrl: string): PrerenderStore {
|
|
60
60
|
return {
|
|
61
|
-
async get(
|
|
61
|
+
async get(routeName, paramHash, meta) {
|
|
62
62
|
if (!meta?.pathname) return null;
|
|
63
63
|
const isIntercept = paramHash.endsWith("/i");
|
|
64
|
-
let url = `${devUrl}/__rsc_prerender?pathname=${encodeURIComponent(meta.pathname)}`;
|
|
64
|
+
let url = `${devUrl}/__rsc_prerender?pathname=${encodeURIComponent(meta.pathname)}&routeName=${encodeURIComponent(routeName)}`;
|
|
65
65
|
if (isIntercept) url += "&intercept=1";
|
|
66
|
+
if (meta.isPassthroughRoute) url += "&passthrough=1";
|
|
66
67
|
try {
|
|
67
68
|
const res = await fetch(url);
|
|
68
69
|
if (!res.ok) return null;
|