@ivogt/rsc-router 0.0.0-experimental.1

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.
Files changed (123) hide show
  1. package/README.md +19 -0
  2. package/package.json +131 -0
  3. package/src/__mocks__/version.ts +6 -0
  4. package/src/__tests__/route-definition.test.ts +63 -0
  5. package/src/browser/event-controller.ts +876 -0
  6. package/src/browser/index.ts +18 -0
  7. package/src/browser/link-interceptor.ts +121 -0
  8. package/src/browser/lru-cache.ts +69 -0
  9. package/src/browser/merge-segment-loaders.ts +126 -0
  10. package/src/browser/navigation-bridge.ts +891 -0
  11. package/src/browser/navigation-client.ts +155 -0
  12. package/src/browser/navigation-store.ts +823 -0
  13. package/src/browser/partial-update.ts +545 -0
  14. package/src/browser/react/Link.tsx +248 -0
  15. package/src/browser/react/NavigationProvider.tsx +228 -0
  16. package/src/browser/react/ScrollRestoration.tsx +94 -0
  17. package/src/browser/react/context.ts +53 -0
  18. package/src/browser/react/index.ts +52 -0
  19. package/src/browser/react/location-state-shared.ts +120 -0
  20. package/src/browser/react/location-state.ts +62 -0
  21. package/src/browser/react/use-action.ts +240 -0
  22. package/src/browser/react/use-client-cache.ts +56 -0
  23. package/src/browser/react/use-handle.ts +178 -0
  24. package/src/browser/react/use-link-status.ts +134 -0
  25. package/src/browser/react/use-navigation.ts +150 -0
  26. package/src/browser/react/use-segments.ts +188 -0
  27. package/src/browser/request-controller.ts +149 -0
  28. package/src/browser/rsc-router.tsx +310 -0
  29. package/src/browser/scroll-restoration.ts +324 -0
  30. package/src/browser/server-action-bridge.ts +747 -0
  31. package/src/browser/shallow.ts +35 -0
  32. package/src/browser/types.ts +443 -0
  33. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  34. package/src/cache/__tests__/memory-store.test.ts +484 -0
  35. package/src/cache/cache-scope.ts +565 -0
  36. package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
  37. package/src/cache/cf/cf-cache-store.ts +274 -0
  38. package/src/cache/cf/index.ts +19 -0
  39. package/src/cache/index.ts +52 -0
  40. package/src/cache/memory-segment-store.ts +150 -0
  41. package/src/cache/memory-store.ts +253 -0
  42. package/src/cache/types.ts +366 -0
  43. package/src/client.rsc.tsx +88 -0
  44. package/src/client.tsx +609 -0
  45. package/src/components/DefaultDocument.tsx +20 -0
  46. package/src/default-error-boundary.tsx +88 -0
  47. package/src/deps/browser.ts +8 -0
  48. package/src/deps/html-stream-client.ts +2 -0
  49. package/src/deps/html-stream-server.ts +2 -0
  50. package/src/deps/rsc.ts +10 -0
  51. package/src/deps/ssr.ts +2 -0
  52. package/src/errors.ts +259 -0
  53. package/src/handle.ts +120 -0
  54. package/src/handles/MetaTags.tsx +178 -0
  55. package/src/handles/index.ts +6 -0
  56. package/src/handles/meta.ts +247 -0
  57. package/src/href-client.ts +128 -0
  58. package/src/href.ts +139 -0
  59. package/src/index.rsc.ts +69 -0
  60. package/src/index.ts +84 -0
  61. package/src/loader.rsc.ts +204 -0
  62. package/src/loader.ts +47 -0
  63. package/src/network-error-thrower.tsx +21 -0
  64. package/src/outlet-context.ts +15 -0
  65. package/src/root-error-boundary.tsx +277 -0
  66. package/src/route-content-wrapper.tsx +198 -0
  67. package/src/route-definition.ts +1333 -0
  68. package/src/route-map-builder.ts +140 -0
  69. package/src/route-types.ts +148 -0
  70. package/src/route-utils.ts +89 -0
  71. package/src/router/__tests__/match-context.test.ts +104 -0
  72. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  73. package/src/router/__tests__/match-result.test.ts +566 -0
  74. package/src/router/__tests__/on-error.test.ts +935 -0
  75. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  76. package/src/router/error-handling.ts +287 -0
  77. package/src/router/handler-context.ts +60 -0
  78. package/src/router/loader-resolution.ts +326 -0
  79. package/src/router/manifest.ts +116 -0
  80. package/src/router/match-context.ts +261 -0
  81. package/src/router/match-middleware/background-revalidation.ts +236 -0
  82. package/src/router/match-middleware/cache-lookup.ts +261 -0
  83. package/src/router/match-middleware/cache-store.ts +250 -0
  84. package/src/router/match-middleware/index.ts +81 -0
  85. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  86. package/src/router/match-middleware/segment-resolution.ts +174 -0
  87. package/src/router/match-pipelines.ts +214 -0
  88. package/src/router/match-result.ts +212 -0
  89. package/src/router/metrics.ts +62 -0
  90. package/src/router/middleware.test.ts +1355 -0
  91. package/src/router/middleware.ts +748 -0
  92. package/src/router/pattern-matching.ts +271 -0
  93. package/src/router/revalidation.ts +190 -0
  94. package/src/router/router-context.ts +299 -0
  95. package/src/router/types.ts +96 -0
  96. package/src/router.ts +3484 -0
  97. package/src/rsc/__tests__/helpers.test.ts +175 -0
  98. package/src/rsc/handler.ts +942 -0
  99. package/src/rsc/helpers.ts +64 -0
  100. package/src/rsc/index.ts +56 -0
  101. package/src/rsc/nonce.ts +18 -0
  102. package/src/rsc/types.ts +225 -0
  103. package/src/segment-system.tsx +405 -0
  104. package/src/server/__tests__/request-context.test.ts +171 -0
  105. package/src/server/context.ts +340 -0
  106. package/src/server/handle-store.ts +230 -0
  107. package/src/server/loader-registry.ts +174 -0
  108. package/src/server/request-context.ts +470 -0
  109. package/src/server/root-layout.tsx +10 -0
  110. package/src/server/tsconfig.json +14 -0
  111. package/src/server.ts +126 -0
  112. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  113. package/src/ssr/index.tsx +215 -0
  114. package/src/types.ts +1473 -0
  115. package/src/use-loader.tsx +346 -0
  116. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  117. package/src/vite/expose-action-id.ts +344 -0
  118. package/src/vite/expose-handle-id.ts +209 -0
  119. package/src/vite/expose-loader-id.ts +357 -0
  120. package/src/vite/expose-location-state-id.ts +177 -0
  121. package/src/vite/index.ts +608 -0
  122. package/src/vite/version.d.ts +12 -0
  123. package/src/vite/virtual-entries.ts +109 -0
@@ -0,0 +1,64 @@
1
+ /**
2
+ * RSC Handler Helpers
3
+ *
4
+ * Utility functions for RSC request handling.
5
+ */
6
+
7
+ import { getRequestContext } from "../server/request-context.js";
8
+
9
+ /**
10
+ * Check if a request body has content to decode
11
+ */
12
+ export function hasBodyContent(body: FormData | string): boolean {
13
+ if (body instanceof FormData) {
14
+ let hasContent = false;
15
+ body.forEach(() => {
16
+ hasContent = true;
17
+ });
18
+ return hasContent;
19
+ }
20
+ return typeof body === "string" && body.length > 0;
21
+ }
22
+
23
+ /**
24
+ * Create a Response with headers merged from the request context's stub response.
25
+ * This ensures headers/cookies set during middleware or handler execution are included.
26
+ * Also triggers any registered onResponse callbacks.
27
+ */
28
+ export function createResponseWithMergedHeaders(
29
+ body: BodyInit | null,
30
+ init: ResponseInit
31
+ ): Response {
32
+ const ctx = getRequestContext();
33
+ if (!ctx) {
34
+ return new Response(body, init);
35
+ }
36
+
37
+ // Merge headers from stub response into the new response
38
+ const mergedHeaders = new Headers(init.headers);
39
+ ctx.res.headers.forEach((value, name) => {
40
+ if (name.toLowerCase() === "set-cookie") {
41
+ mergedHeaders.append(name, value);
42
+ } else if (!mergedHeaders.has(name)) {
43
+ // Only set if not already present in init.headers
44
+ mergedHeaders.set(name, value);
45
+ }
46
+ });
47
+
48
+ // Use ctx.res.status if it was set (e.g., 404 for notFound, 500 for error)
49
+ // Otherwise use the status from init
50
+ const status = ctx.res.status !== 200 ? ctx.res.status : init.status;
51
+
52
+ let response = new Response(body, {
53
+ ...init,
54
+ status,
55
+ headers: mergedHeaders,
56
+ });
57
+
58
+ // Run onResponse callbacks - each can inspect/modify the response
59
+ for (const callback of ctx._onResponseCallbacks) {
60
+ response = callback(response) ?? response;
61
+ }
62
+
63
+ return response;
64
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * RSC Router - RSC Handler Entry Point
3
+ *
4
+ * This module provides the RSC request handler for server-side rendering,
5
+ * server actions, loader fetching, and progressive enhancement.
6
+ *
7
+ * @example Basic usage
8
+ * ```tsx
9
+ * import { createRSCHandler } from "rsc-router/rsc";
10
+ * import { router } from "./router.js";
11
+ *
12
+ * export default createRSCHandler({ router });
13
+ * ```
14
+ */
15
+
16
+ // Re-export handler
17
+ export { createRSCHandler } from "./handler.js";
18
+
19
+ // Re-export types
20
+ export type {
21
+ RscPayload,
22
+ ReactFormState,
23
+ RSCDependencies,
24
+ SSRRenderOptions,
25
+ SSRModule,
26
+ LoadSSRModule,
27
+ CreateRSCHandlerOptions,
28
+ HandlerCacheConfig,
29
+ NonceProvider,
30
+ } from "./types.js";
31
+
32
+ // Re-export HandleStore types for consumers who need custom handling
33
+ export {
34
+ createHandleStore,
35
+ type HandleStore,
36
+ type HandleData,
37
+ } from "../server/handle-store.js";
38
+
39
+ // Re-export request context utilities for server-side access to env/request/params
40
+ export {
41
+ getRequestContext,
42
+ requireRequestContext,
43
+ setRequestContextParams,
44
+ } from "../server/request-context.js";
45
+
46
+ // Re-export cache store types and implementations
47
+ export type {
48
+ SegmentCacheStore,
49
+ CachedEntryData,
50
+ CachedEntryResult,
51
+ SegmentCacheProvider,
52
+ SegmentHandleData,
53
+ } from "../cache/types.js";
54
+
55
+ export { MemorySegmentCacheStore } from "../cache/memory-segment-store.js";
56
+ export { CFCacheStore, type CFCacheStoreOptions } from "../cache/cf/index.js";
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Nonce generation for Content Security Policy (CSP)
3
+ */
4
+
5
+ /**
6
+ * Generate a cryptographic nonce for CSP.
7
+ * Returns a 16-byte random value encoded as base64.
8
+ */
9
+ export function generateNonce(): string {
10
+ const array = new Uint8Array(16);
11
+ crypto.getRandomValues(array);
12
+ // Convert to base64
13
+ let binary = "";
14
+ for (let i = 0; i < array.length; i++) {
15
+ binary += String.fromCharCode(array[i]);
16
+ }
17
+ return btoa(binary);
18
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * RSC Handler Types
3
+ *
4
+ * Type definitions for the RSC request handler, payload structures,
5
+ * and SSR integration.
6
+ */
7
+
8
+ import type { ResolvedSegment, SlotState } from "../types.js";
9
+ import type { HandleData } from "../server/handle-store.js";
10
+ import type { RSCRouter } from "../router.js";
11
+
12
+ /**
13
+ * RSC payload sent to the client
14
+ */
15
+ export interface RscPayload {
16
+ root: React.ReactNode | Promise<React.ReactNode>;
17
+ metadata?: {
18
+ pathname: string;
19
+ segments: ResolvedSegment[];
20
+ isPartial?: boolean;
21
+ isError?: boolean;
22
+ matched?: string[];
23
+ diff?: string[];
24
+ slots?: Record<string, SlotState>;
25
+ /** Root layout component for browser-side re-renders (client component reference) */
26
+ rootLayout?: React.ComponentType<{ children: React.ReactNode }>;
27
+ /** Handle data accumulated across route segments (async generator that yields on each push) */
28
+ handles?: AsyncGenerator<HandleData, void, unknown>;
29
+ /** RSC version string for cache invalidation */
30
+ version?: string;
31
+ };
32
+ returnValue?: { ok: boolean; data: unknown };
33
+ formState?: unknown;
34
+ }
35
+
36
+ /**
37
+ * React form state type for useActionState progressive enhancement
38
+ */
39
+ export type ReactFormState = unknown;
40
+
41
+ /**
42
+ * RSC dependencies from @vitejs/plugin-rsc/rsc
43
+ */
44
+ export interface RSCDependencies {
45
+ /**
46
+ * renderToReadableStream from @vitejs/plugin-rsc/rsc
47
+ */
48
+ renderToReadableStream: <T>(
49
+ payload: T,
50
+ options?: { temporaryReferences?: unknown }
51
+ ) => ReadableStream<Uint8Array>;
52
+
53
+ /**
54
+ * decodeReply from @vitejs/plugin-rsc/rsc
55
+ */
56
+ decodeReply: (
57
+ body: FormData | string,
58
+ options?: { temporaryReferences?: unknown }
59
+ ) => Promise<unknown[]>;
60
+
61
+ /**
62
+ * createTemporaryReferenceSet from @vitejs/plugin-rsc/rsc
63
+ */
64
+ createTemporaryReferenceSet: () => unknown;
65
+
66
+ /**
67
+ * loadServerAction from @vitejs/plugin-rsc/rsc
68
+ */
69
+ loadServerAction: (actionId: string) => Promise<Function>;
70
+
71
+ /**
72
+ * decodeAction from @vitejs/plugin-rsc/rsc
73
+ * Decodes a FormData into a bound action function (for useActionState forms)
74
+ */
75
+ decodeAction: (body: FormData) => Promise<() => Promise<unknown>>;
76
+
77
+ /**
78
+ * decodeFormState from @vitejs/plugin-rsc/rsc
79
+ * Decodes the action result into a ReactFormState for useActionState progressive enhancement
80
+ */
81
+ decodeFormState: (
82
+ actionResult: unknown,
83
+ body: FormData
84
+ ) => Promise<ReactFormState | null>;
85
+ }
86
+
87
+ /**
88
+ * Options for SSR HTML rendering
89
+ */
90
+ export interface SSRRenderOptions {
91
+ /**
92
+ * Form state for useActionState progressive enhancement.
93
+ * This is the result of decodeFormState() and should be passed to
94
+ * react-dom's renderToReadableStream to enable useActionState to
95
+ * receive the action result during SSR.
96
+ */
97
+ formState?: ReactFormState | null;
98
+
99
+ /**
100
+ * Nonce for Content Security Policy (CSP)
101
+ */
102
+ nonce?: string;
103
+ }
104
+
105
+ /**
106
+ * SSR module interface for HTML rendering
107
+ */
108
+ export interface SSRModule {
109
+ renderHTML: (
110
+ rscStream: ReadableStream<Uint8Array>,
111
+ options?: SSRRenderOptions
112
+ ) => Promise<ReadableStream<Uint8Array>>;
113
+ }
114
+
115
+ /**
116
+ * Function to load SSR module dynamically
117
+ */
118
+ export type LoadSSRModule = () => Promise<SSRModule>;
119
+
120
+ /**
121
+ * Cache configuration for handler.
122
+ * TTL is configured via store.defaults or cache() boundaries.
123
+ */
124
+ export interface HandlerCacheConfig {
125
+ /** Cache store implementation */
126
+ store: import("../cache/types.js").SegmentCacheStore;
127
+ /** Enable/disable caching (default: true) */
128
+ enabled?: boolean;
129
+ }
130
+
131
+ /**
132
+ * Nonce provider function type.
133
+ * Can return a nonce string, or true to auto-generate one.
134
+ */
135
+ export type NonceProvider<TEnv = unknown> = (
136
+ request: Request,
137
+ env: TEnv
138
+ ) => string | true | Promise<string | true>;
139
+
140
+ /**
141
+ * Options for creating an RSC handler
142
+ */
143
+ export interface CreateRSCHandlerOptions<TEnv = unknown> {
144
+ /**
145
+ * The RSC router instance
146
+ */
147
+ router: RSCRouter<TEnv>;
148
+
149
+ /**
150
+ * RSC dependencies from @vitejs/plugin-rsc/rsc.
151
+ * Defaults to the exports from @vitejs/plugin-rsc/rsc.
152
+ */
153
+ deps?: RSCDependencies;
154
+
155
+ /**
156
+ * Function to load the SSR module for HTML rendering.
157
+ * Defaults to: () => import.meta.viteRsc.loadModule("ssr", "index")
158
+ */
159
+ loadSSRModule?: LoadSSRModule;
160
+
161
+ /**
162
+ * Cache configuration for segment caching.
163
+ *
164
+ * Can be a static config object or a function that receives the env
165
+ * (useful for accessing Cloudflare bindings).
166
+ *
167
+ * If not provided, caching is disabled. TTL is configured via store.defaults
168
+ * or cache() boundaries in the route definition.
169
+ *
170
+ * @example Static config
171
+ * ```typescript
172
+ * cache: {
173
+ * store: new MemorySegmentCacheStore({ defaults: { ttl: 60 } }),
174
+ * }
175
+ * ```
176
+ *
177
+ * @example Dynamic config with env
178
+ * ```typescript
179
+ * cache: (env) => ({
180
+ * store: new KVSegmentCacheStore(env.Bindings.MY_CACHE, { defaults: { ttl: 60 } }),
181
+ * })
182
+ * ```
183
+ */
184
+ cache?: HandlerCacheConfig | ((env: TEnv) => HandlerCacheConfig);
185
+
186
+ /**
187
+ * RSC version string included in metadata.
188
+ * The browser sends this back on partial requests to detect version mismatches.
189
+ *
190
+ * Defaults to the auto-generated VERSION from `rsc-router:version` virtual module.
191
+ * Only set this if you need a custom versioning strategy.
192
+ *
193
+ * @default VERSION from rsc-router:version
194
+ */
195
+ version?: string;
196
+
197
+ /**
198
+ * Nonce provider for Content Security Policy (CSP).
199
+ *
200
+ * Can be:
201
+ * - A function that returns a nonce string
202
+ * - A function that returns `true` to auto-generate a nonce
203
+ * - Undefined to disable nonce (default)
204
+ *
205
+ * The nonce will be applied to inline scripts injected by the RSC payload.
206
+ * It's also available to middleware via `ctx.get('nonce')`.
207
+ *
208
+ * @example Auto-generate nonce
209
+ * ```tsx
210
+ * createRSCHandler({
211
+ * router,
212
+ * nonce: () => true,
213
+ * });
214
+ * ```
215
+ *
216
+ * @example Custom nonce from request context
217
+ * ```tsx
218
+ * createRSCHandler({
219
+ * router,
220
+ * nonce: (request, env) => env.nonce,
221
+ * });
222
+ * ```
223
+ */
224
+ nonce?: NonceProvider<TEnv>;
225
+ }