@rangojs/router 0.0.0-experimental.259 → 0.0.0-experimental.26
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 +294 -28
- package/dist/bin/rango.js +355 -47
- package/dist/vite/index.js +1658 -1239
- package/package.json +3 -3
- package/skills/cache-guide/SKILL.md +9 -5
- package/skills/caching/SKILL.md +4 -4
- package/skills/document-cache/SKILL.md +2 -2
- package/skills/hooks/SKILL.md +40 -29
- package/skills/host-router/SKILL.md +218 -0
- package/skills/intercept/SKILL.md +79 -0
- package/skills/layout/SKILL.md +62 -2
- package/skills/loader/SKILL.md +229 -15
- package/skills/middleware/SKILL.md +109 -30
- package/skills/parallel/SKILL.md +57 -2
- package/skills/prerender/SKILL.md +189 -19
- package/skills/rango/SKILL.md +1 -2
- package/skills/response-routes/SKILL.md +3 -3
- package/skills/route/SKILL.md +44 -3
- package/skills/router-setup/SKILL.md +80 -3
- package/skills/theme/SKILL.md +5 -4
- package/skills/typesafety/SKILL.md +59 -16
- package/skills/use-cache/SKILL.md +16 -2
- package/src/__internal.ts +1 -1
- package/src/bin/rango.ts +56 -19
- package/src/browser/action-coordinator.ts +97 -0
- package/src/browser/event-controller.ts +29 -48
- package/src/browser/history-state.ts +80 -0
- package/src/browser/intercept-utils.ts +1 -1
- package/src/browser/link-interceptor.ts +19 -3
- package/src/browser/merge-segment-loaders.ts +9 -2
- package/src/browser/navigation-bridge.ts +66 -443
- package/src/browser/navigation-client.ts +34 -62
- package/src/browser/navigation-store.ts +4 -33
- package/src/browser/navigation-transaction.ts +295 -0
- package/src/browser/partial-update.ts +103 -151
- package/src/browser/prefetch/cache.ts +67 -0
- package/src/browser/prefetch/fetch.ts +137 -0
- package/src/browser/prefetch/observer.ts +65 -0
- package/src/browser/prefetch/policy.ts +42 -0
- package/src/browser/prefetch/queue.ts +88 -0
- package/src/browser/rango-state.ts +112 -0
- package/src/browser/react/Link.tsx +154 -44
- package/src/browser/react/NavigationProvider.tsx +32 -0
- package/src/browser/react/context.ts +6 -0
- package/src/browser/react/filter-segment-order.ts +11 -0
- package/src/browser/react/index.ts +2 -6
- package/src/browser/react/location-state-shared.ts +29 -11
- package/src/browser/react/location-state.ts +6 -4
- package/src/browser/react/nonce-context.ts +23 -0
- package/src/browser/react/shallow-equal.ts +27 -0
- package/src/browser/react/use-action.ts +23 -45
- package/src/browser/react/use-client-cache.ts +5 -3
- package/src/browser/react/use-handle.ts +21 -64
- package/src/browser/react/use-navigation.ts +7 -32
- package/src/browser/react/use-params.ts +5 -34
- package/src/browser/react/use-pathname.ts +2 -3
- package/src/browser/react/use-router.ts +3 -6
- package/src/browser/react/use-search-params.ts +2 -1
- package/src/browser/react/use-segments.ts +75 -114
- package/src/browser/response-adapter.ts +73 -0
- package/src/browser/rsc-router.tsx +46 -22
- package/src/browser/scroll-restoration.ts +10 -7
- package/src/browser/server-action-bridge.ts +458 -405
- package/src/browser/types.ts +21 -35
- package/src/browser/validate-redirect-origin.ts +29 -0
- package/src/build/generate-manifest.ts +38 -13
- package/src/build/generate-route-types.ts +4 -0
- package/src/build/index.ts +1 -0
- package/src/build/route-trie.ts +19 -3
- 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 +170 -18
- 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 +136 -123
- package/src/cache/cache-scope.ts +76 -83
- package/src/cache/cf/cf-cache-store.ts +12 -7
- package/src/cache/document-cache.ts +93 -69
- package/src/cache/handle-capture.ts +81 -0
- package/src/cache/index.ts +0 -15
- package/src/cache/memory-segment-store.ts +43 -69
- package/src/cache/profile-registry.ts +43 -8
- package/src/cache/read-through-swr.ts +134 -0
- package/src/cache/segment-codec.ts +140 -117
- package/src/cache/taint.ts +30 -3
- package/src/cache/types.ts +1 -115
- package/src/client.rsc.tsx +0 -1
- package/src/client.tsx +53 -76
- 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/index.ts +0 -3
- package/src/host/router.ts +14 -1
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +53 -10
- package/src/index.ts +73 -43
- package/src/loader.rsc.ts +12 -4
- package/src/loader.ts +8 -0
- package/src/prerender/store.ts +60 -18
- 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/index.ts +0 -3
- 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 +96 -17
- package/src/router/intercept-resolution.ts +6 -4
- package/src/router/lazy-includes.ts +4 -0
- package/src/router/loader-resolution.ts +6 -11
- package/src/router/logging.ts +100 -3
- package/src/router/manifest.ts +32 -3
- package/src/router/match-api.ts +62 -54
- 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 +78 -10
- package/src/router/match-middleware/cache-store.ts +2 -0
- package/src/router/match-pipelines.ts +8 -43
- package/src/router/match-result.ts +0 -9
- package/src/router/metrics.ts +233 -13
- package/src/router/middleware-types.ts +34 -39
- package/src/router/middleware.ts +290 -130
- package/src/router/pattern-matching.ts +61 -10
- package/src/router/prerender-match.ts +36 -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 +158 -40
- package/src/router/router-options.ts +223 -1
- package/src/router/router-registry.ts +5 -2
- package/src/router/segment-resolution/fresh.ts +165 -242
- package/src/router/segment-resolution/helpers.ts +263 -0
- package/src/router/segment-resolution/loader-cache.ts +102 -98
- package/src/router/segment-resolution/revalidation.ts +394 -272
- package/src/router/segment-resolution/static-store.ts +2 -2
- package/src/router/segment-resolution.ts +1 -3
- 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/trie-matching.ts +20 -2
- package/src/router/types.ts +7 -1
- package/src/router.ts +203 -18
- package/src/rsc/handler-context.ts +13 -2
- package/src/rsc/handler.ts +489 -438
- package/src/rsc/helpers.ts +125 -5
- package/src/rsc/index.ts +0 -20
- package/src/rsc/loader-fetch.ts +84 -42
- package/src/rsc/manifest-init.ts +3 -2
- package/src/rsc/origin-guard.ts +141 -0
- package/src/rsc/progressive-enhancement.ts +245 -19
- package/src/rsc/response-route-handler.ts +347 -0
- package/src/rsc/rsc-rendering.ts +47 -43
- package/src/rsc/runtime-warnings.ts +42 -0
- package/src/rsc/server-action.ts +166 -66
- package/src/rsc/ssr-setup.ts +128 -0
- package/src/rsc/types.ts +20 -2
- package/src/search-params.ts +38 -23
- package/src/server/context.ts +61 -7
- package/src/server/cookie-store.ts +190 -0
- package/src/server/fetchable-loader-store.ts +11 -6
- package/src/server/handle-store.ts +84 -12
- package/src/server/loader-registry.ts +11 -46
- package/src/server/request-context.ts +275 -49
- package/src/server.ts +6 -0
- package/src/ssr/index.tsx +67 -28
- package/src/static-handler.ts +7 -0
- package/src/theme/ThemeProvider.tsx +6 -1
- package/src/theme/index.ts +4 -18
- package/src/theme/theme-context.ts +1 -28
- package/src/theme/theme-script.ts +2 -1
- package/src/types/cache-types.ts +6 -1
- package/src/types/error-types.ts +3 -0
- package/src/types/global-namespace.ts +22 -0
- package/src/types/handler-context.ts +103 -16
- package/src/types/index.ts +1 -1
- package/src/types/loader-types.ts +9 -6
- package/src/types/route-config.ts +17 -26
- package/src/types/route-entry.ts +28 -0
- package/src/types/segments.ts +0 -5
- 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 +29 -7
- package/src/urls/type-extraction.ts +23 -15
- package/src/use-loader.tsx +27 -9
- package/src/vite/discovery/bundle-postprocess.ts +32 -52
- package/src/vite/discovery/discover-routers.ts +52 -26
- package/src/vite/discovery/prerender-collection.ts +58 -41
- package/src/vite/discovery/route-types-writer.ts +7 -7
- package/src/vite/discovery/state.ts +7 -7
- package/src/vite/discovery/virtual-module-codegen.ts +5 -2
- package/src/vite/index.ts +10 -51
- package/src/vite/plugins/client-ref-dedup.ts +115 -0
- package/src/vite/plugins/client-ref-hashing.ts +3 -3
- package/src/vite/plugins/expose-internal-ids.ts +4 -3
- package/src/vite/plugins/refresh-cmd.ts +65 -0
- package/src/vite/plugins/use-cache-transform.ts +91 -3
- package/src/vite/plugins/version-plugin.ts +188 -18
- package/src/vite/rango.ts +61 -36
- package/src/vite/router-discovery.ts +173 -100
- package/src/vite/utils/prerender-utils.ts +81 -0
- package/src/vite/utils/shared-utils.ts +19 -9
- package/skills/testing/SKILL.md +0 -226
- package/src/browser/lru-cache.ts +0 -61
- package/src/browser/react/prefetch.ts +0 -27
- package/src/browser/request-controller.ts +0 -164
- package/src/cache/memory-store.ts +0 -253
- package/src/href-context.ts +0 -33
- package/src/route-definition/route-function.ts +0 -119
- package/src/router.gen.ts +0 -6
- package/src/static-handler.gen.ts +0 -5
- package/src/urls.gen.ts +0 -8
- /package/{CLAUDE.md → AGENTS.md} +0 -0
package/src/cache/taint.ts
CHANGED
|
@@ -22,16 +22,43 @@ export function isTainted(value: unknown): boolean {
|
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* Symbol stamped on tainted ctx during "use cache" function execution.
|
|
25
|
-
*
|
|
26
|
-
* throw if present —
|
|
25
|
+
* cookies(), headers(), ctx.set(), ctx.header(), etc. check this flag and
|
|
26
|
+
* throw if present — reads would cache per-request data under a shared key,
|
|
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.
|
|
27
32
|
*/
|
|
28
33
|
export const INSIDE_CACHE_EXEC: unique symbol = Symbol.for(
|
|
29
34
|
"rango:inside-cache-exec",
|
|
30
35
|
) as any;
|
|
31
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
|
+
|
|
32
59
|
/**
|
|
33
60
|
* Throw if ctx is inside a "use cache" execution.
|
|
34
|
-
* Call from side-effecting ctx methods (set, header,
|
|
61
|
+
* Call from side-effecting ctx methods (set, header, etc.) and cookie mutations.
|
|
35
62
|
*/
|
|
36
63
|
export function assertNotInsideCacheExec(
|
|
37
64
|
ctx: unknown,
|
package/src/cache/types.ts
CHANGED
|
@@ -75,7 +75,7 @@ export interface SegmentCacheStore<TEnv = unknown> {
|
|
|
75
75
|
* @example Using cookies for locale
|
|
76
76
|
* ```typescript
|
|
77
77
|
* keyGenerator: (ctx, defaultKey) => {
|
|
78
|
-
* const locale =
|
|
78
|
+
* const locale = cookies().get('locale')?.value || 'en';
|
|
79
79
|
* return `${locale}:${defaultKey}`;
|
|
80
80
|
* }
|
|
81
81
|
* ```
|
|
@@ -340,117 +340,3 @@ export interface SegmentCacheProvider {
|
|
|
340
340
|
*/
|
|
341
341
|
cacheEntry(cacheKey: string, segments: ResolvedSegment[]): void;
|
|
342
342
|
}
|
|
343
|
-
|
|
344
|
-
// ============================================================================
|
|
345
|
-
// Generic Cache Store (for future extensibility)
|
|
346
|
-
// ============================================================================
|
|
347
|
-
// These types support a general-purpose cache interface that can be used
|
|
348
|
-
// for caching arbitrary values (responses, streams, objects). Currently,
|
|
349
|
-
// the segment caching system uses SegmentCacheStore directly, but these
|
|
350
|
-
// types enable future use cases like response caching or data caching.
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Supported cache value types for the generic CacheStore interface.
|
|
354
|
-
* @internal Reserved for future extensibility
|
|
355
|
-
*/
|
|
356
|
-
export type CacheValue =
|
|
357
|
-
| ReadableStream<Uint8Array>
|
|
358
|
-
| Response
|
|
359
|
-
| ArrayBuffer
|
|
360
|
-
| string
|
|
361
|
-
| unknown[] // JSON-serializable array
|
|
362
|
-
| Record<string, unknown>; // JSON-serializable object
|
|
363
|
-
|
|
364
|
-
/**
|
|
365
|
-
* Cache entry returned by match().
|
|
366
|
-
* @internal Reserved for future extensibility
|
|
367
|
-
*/
|
|
368
|
-
export interface CacheEntry<T = CacheValue> {
|
|
369
|
-
/** The cached value */
|
|
370
|
-
value: T;
|
|
371
|
-
/** Optional metadata stored with the entry */
|
|
372
|
-
metadata?: CacheMetadata;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/**
|
|
376
|
-
* Original value type for reconstruction.
|
|
377
|
-
* @internal Reserved for future extensibility
|
|
378
|
-
*/
|
|
379
|
-
export type CacheValueType =
|
|
380
|
-
| "stream"
|
|
381
|
-
| "response"
|
|
382
|
-
| "arraybuffer"
|
|
383
|
-
| "string"
|
|
384
|
-
| "object";
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Metadata associated with a cache entry.
|
|
388
|
-
* @internal Reserved for future extensibility
|
|
389
|
-
*/
|
|
390
|
-
export interface CacheMetadata {
|
|
391
|
-
/** Timestamp when entry expires (ms since epoch) */
|
|
392
|
-
expiresAt?: number;
|
|
393
|
-
/** Tags for bulk invalidation */
|
|
394
|
-
tags?: string[];
|
|
395
|
-
/** Original value type for reconstruction on read */
|
|
396
|
-
valueType?: CacheValueType;
|
|
397
|
-
/** Response headers (preserved when caching Response) */
|
|
398
|
-
responseHeaders?: Record<string, string>;
|
|
399
|
-
/** Response status (preserved when caching Response) */
|
|
400
|
-
responseStatus?: number;
|
|
401
|
-
/** Custom metadata */
|
|
402
|
-
[key: string]: unknown;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Options for put().
|
|
407
|
-
* @internal Reserved for future extensibility
|
|
408
|
-
*/
|
|
409
|
-
export interface CachePutOptions {
|
|
410
|
-
/** Time-to-live in seconds */
|
|
411
|
-
ttl?: number;
|
|
412
|
-
/** Metadata to store with entry */
|
|
413
|
-
metadata?: Omit<CacheMetadata, "expiresAt">;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Generic cache store interface for arbitrary value types.
|
|
418
|
-
*
|
|
419
|
-
* This interface is designed for future extensibility to support caching
|
|
420
|
-
* responses, streams, and other values. Currently, segment caching uses
|
|
421
|
-
* the SegmentCacheStore interface directly.
|
|
422
|
-
*
|
|
423
|
-
* Implementations must handle:
|
|
424
|
-
* - Stream values (clone before storing, streams can only be read once)
|
|
425
|
-
* - Promise values (await before storing)
|
|
426
|
-
* - Expiration/TTL
|
|
427
|
-
*
|
|
428
|
-
* @internal Reserved for future extensibility
|
|
429
|
-
*/
|
|
430
|
-
export interface CacheStore {
|
|
431
|
-
/**
|
|
432
|
-
* Retrieve a cached entry by key.
|
|
433
|
-
* @param key - Cache key
|
|
434
|
-
* @returns The cached entry or undefined if not found/expired
|
|
435
|
-
*/
|
|
436
|
-
match<T = CacheValue>(key: string): Promise<CacheEntry<T> | undefined>;
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Store a value in the cache.
|
|
440
|
-
* @param key - Cache key
|
|
441
|
-
* @param value - Value to cache (stream, response, string, object, etc.)
|
|
442
|
-
* @param options - TTL, metadata, etc.
|
|
443
|
-
*/
|
|
444
|
-
put<T extends CacheValue>(
|
|
445
|
-
key: string,
|
|
446
|
-
value: T,
|
|
447
|
-
options?: CachePutOptions,
|
|
448
|
-
): Promise<void>;
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Delete a cached entry.
|
|
452
|
-
* @param key - Cache key
|
|
453
|
-
* @returns true if entry was deleted, false if not found
|
|
454
|
-
*/
|
|
455
|
-
delete(key: string): Promise<boolean>;
|
|
456
|
-
}
|
package/src/client.rsc.tsx
CHANGED
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;
|
|
142
|
+
}
|
|
143
|
+
|
|
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
|
+
);
|
|
139
151
|
}
|
|
140
152
|
|
|
141
|
-
return
|
|
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
|
|
@@ -290,52 +313,6 @@ export {
|
|
|
290
313
|
type UseLoaderOptions,
|
|
291
314
|
} from "./use-loader.js";
|
|
292
315
|
|
|
293
|
-
/**
|
|
294
|
-
* Hook to access all loader data in the current context
|
|
295
|
-
*
|
|
296
|
-
* Returns a record of all loader data available in the current outlet context
|
|
297
|
-
* and all parent contexts. Useful for debugging or when you need access to
|
|
298
|
-
* multiple loaders.
|
|
299
|
-
*
|
|
300
|
-
* @returns Record of loader name to data, or empty object if no loaders
|
|
301
|
-
*
|
|
302
|
-
* @example
|
|
303
|
-
* ```tsx
|
|
304
|
-
* "use client";
|
|
305
|
-
* import { useLoaderData } from "rsc-router/client";
|
|
306
|
-
*
|
|
307
|
-
* export function DebugPanel() {
|
|
308
|
-
* const loaderData = useLoaderData();
|
|
309
|
-
* return <pre>{JSON.stringify(loaderData, null, 2)}</pre>;
|
|
310
|
-
* }
|
|
311
|
-
* ```
|
|
312
|
-
*/
|
|
313
|
-
export function useLoaderData(): Record<string, any> {
|
|
314
|
-
const context = useContext(OutletContext);
|
|
315
|
-
|
|
316
|
-
// Collect all loader data from the context chain
|
|
317
|
-
// Child loaders override parent loaders with the same name
|
|
318
|
-
const result: Record<string, any> = {};
|
|
319
|
-
const stack: OutletContextValue[] = [];
|
|
320
|
-
|
|
321
|
-
// Build stack from current to root
|
|
322
|
-
let current: OutletContextValue | null | undefined = context;
|
|
323
|
-
while (current) {
|
|
324
|
-
stack.push(current);
|
|
325
|
-
current = current.parent;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
// Apply from root to current (so children override parents)
|
|
329
|
-
for (let i = stack.length - 1; i >= 0; i--) {
|
|
330
|
-
const ctx = stack[i];
|
|
331
|
-
if (ctx.loaderData) {
|
|
332
|
-
Object.assign(result, ctx.loaderData);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return result;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
316
|
/**
|
|
340
317
|
* Client-safe createLoader factory
|
|
341
318
|
*
|
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/index.ts
CHANGED
|
@@ -25,9 +25,6 @@
|
|
|
25
25
|
// Core router
|
|
26
26
|
export { createHostRouter } from "./router.js";
|
|
27
27
|
|
|
28
|
-
// Host router registry for build-time discovery
|
|
29
|
-
export { HostRouterRegistry, type HostRouterRegistryEntry } from "./router.js";
|
|
30
|
-
|
|
31
28
|
// Utilities
|
|
32
29
|
export { defineHosts } from "./utils.js";
|
|
33
30
|
|
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
|
@@ -33,7 +33,6 @@ export {
|
|
|
33
33
|
export type {
|
|
34
34
|
// Configuration types
|
|
35
35
|
DocumentProps,
|
|
36
|
-
RouterEnv,
|
|
37
36
|
DefaultEnv,
|
|
38
37
|
RouteDefinition,
|
|
39
38
|
RouteConfig,
|
|
@@ -73,7 +72,12 @@ export type {
|
|
|
73
72
|
} from "./types.js";
|
|
74
73
|
|
|
75
74
|
// Router options type (server-only, so import directly)
|
|
76
|
-
export type {
|
|
75
|
+
export type {
|
|
76
|
+
RSCRouterOptions,
|
|
77
|
+
SSRStreamMode,
|
|
78
|
+
SSROptions,
|
|
79
|
+
ResolveStreamingContext,
|
|
80
|
+
} from "./router.js";
|
|
77
81
|
|
|
78
82
|
// Server-side createLoader and redirect
|
|
79
83
|
export {
|
|
@@ -168,12 +172,27 @@ export type { HandlerCacheConfig } from "./rsc/types.js";
|
|
|
168
172
|
// Built-in handles (server-side)
|
|
169
173
|
export { Meta } from "./handles/meta.js";
|
|
170
174
|
|
|
171
|
-
// Request context (for accessing request data in server actions/components)
|
|
175
|
+
// Request context (for accessing request data in server actions/components).
|
|
176
|
+
// Re-exported with a narrowed return type so that public consumers only see
|
|
177
|
+
// public members. Internal code imports from "./server/request-context.js"
|
|
178
|
+
// directly and gets the full type.
|
|
179
|
+
import { getRequestContext as _getRequestContextInternal } from "./server/request-context.js";
|
|
180
|
+
export type { PublicRequestContext as RequestContext } from "./server/request-context.js";
|
|
181
|
+
import type { PublicRequestContext } from "./server/request-context.js";
|
|
182
|
+
import type { DefaultEnv } from "./types/global-namespace.js";
|
|
183
|
+
|
|
184
|
+
export const getRequestContext: <
|
|
185
|
+
TEnv = DefaultEnv,
|
|
186
|
+
>() => PublicRequestContext<TEnv> = _getRequestContextInternal;
|
|
187
|
+
|
|
188
|
+
// Request-scoped shorthands
|
|
172
189
|
export {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
type
|
|
176
|
-
|
|
190
|
+
cookies,
|
|
191
|
+
headers,
|
|
192
|
+
type CookieStore,
|
|
193
|
+
type Cookie,
|
|
194
|
+
type ReadonlyHeaders,
|
|
195
|
+
} from "./server/cookie-store.js";
|
|
177
196
|
|
|
178
197
|
// Meta types
|
|
179
198
|
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
@@ -201,9 +220,6 @@ export type {
|
|
|
201
220
|
RouteParams,
|
|
202
221
|
} from "./search-params.js";
|
|
203
222
|
|
|
204
|
-
// Performance tracking (server-only)
|
|
205
|
-
export { track } from "./server/context.js";
|
|
206
|
-
|
|
207
223
|
// Debug utilities for route matching (development only)
|
|
208
224
|
export {
|
|
209
225
|
enableMatchDebug,
|
|
@@ -220,3 +236,30 @@ export {
|
|
|
220
236
|
|
|
221
237
|
// Path-based response type lookup from RegisteredRoutes
|
|
222
238
|
export type { PathResponse } from "./href-client.js";
|
|
239
|
+
|
|
240
|
+
// Telemetry sink
|
|
241
|
+
export { createConsoleSink } from "./router/telemetry.js";
|
|
242
|
+
export { createOTelSink } from "./router/telemetry-otel.js";
|
|
243
|
+
export type { OTelTracer, OTelSpan } from "./router/telemetry-otel.js";
|
|
244
|
+
export type {
|
|
245
|
+
TelemetrySink,
|
|
246
|
+
TelemetryEvent,
|
|
247
|
+
RequestStartEvent,
|
|
248
|
+
RequestEndEvent,
|
|
249
|
+
RequestErrorEvent,
|
|
250
|
+
RequestTimeoutEvent,
|
|
251
|
+
LoaderStartEvent,
|
|
252
|
+
LoaderEndEvent,
|
|
253
|
+
LoaderErrorEvent,
|
|
254
|
+
HandlerErrorEvent,
|
|
255
|
+
CacheDecisionEvent,
|
|
256
|
+
RevalidationDecisionEvent,
|
|
257
|
+
} from "./router/telemetry.js";
|
|
258
|
+
|
|
259
|
+
// Timeout types and error class
|
|
260
|
+
export { RouterTimeoutError } from "./router/timeout.js";
|
|
261
|
+
export type {
|
|
262
|
+
RouterTimeouts,
|
|
263
|
+
TimeoutPhase,
|
|
264
|
+
TimeoutContext,
|
|
265
|
+
} from "./router/timeout.js";
|