@rangojs/router 0.0.0-experimental.2

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 (155) hide show
  1. package/CLAUDE.md +7 -0
  2. package/README.md +19 -0
  3. package/dist/vite/index.js +1298 -0
  4. package/package.json +140 -0
  5. package/skills/caching/SKILL.md +319 -0
  6. package/skills/document-cache/SKILL.md +152 -0
  7. package/skills/hooks/SKILL.md +359 -0
  8. package/skills/intercept/SKILL.md +292 -0
  9. package/skills/layout/SKILL.md +216 -0
  10. package/skills/loader/SKILL.md +365 -0
  11. package/skills/middleware/SKILL.md +442 -0
  12. package/skills/parallel/SKILL.md +255 -0
  13. package/skills/route/SKILL.md +141 -0
  14. package/skills/router-setup/SKILL.md +403 -0
  15. package/skills/theme/SKILL.md +54 -0
  16. package/skills/typesafety/SKILL.md +352 -0
  17. package/src/__mocks__/version.ts +6 -0
  18. package/src/__tests__/component-utils.test.ts +76 -0
  19. package/src/__tests__/route-definition.test.ts +63 -0
  20. package/src/__tests__/urls.test.tsx +436 -0
  21. package/src/browser/event-controller.ts +876 -0
  22. package/src/browser/index.ts +18 -0
  23. package/src/browser/link-interceptor.ts +121 -0
  24. package/src/browser/lru-cache.ts +69 -0
  25. package/src/browser/merge-segment-loaders.ts +126 -0
  26. package/src/browser/navigation-bridge.ts +893 -0
  27. package/src/browser/navigation-client.ts +162 -0
  28. package/src/browser/navigation-store.ts +823 -0
  29. package/src/browser/partial-update.ts +559 -0
  30. package/src/browser/react/Link.tsx +248 -0
  31. package/src/browser/react/NavigationProvider.tsx +275 -0
  32. package/src/browser/react/ScrollRestoration.tsx +94 -0
  33. package/src/browser/react/context.ts +53 -0
  34. package/src/browser/react/index.ts +52 -0
  35. package/src/browser/react/location-state-shared.ts +120 -0
  36. package/src/browser/react/location-state.ts +62 -0
  37. package/src/browser/react/use-action.ts +240 -0
  38. package/src/browser/react/use-client-cache.ts +56 -0
  39. package/src/browser/react/use-handle.ts +178 -0
  40. package/src/browser/react/use-href.tsx +208 -0
  41. package/src/browser/react/use-link-status.ts +134 -0
  42. package/src/browser/react/use-navigation.ts +150 -0
  43. package/src/browser/react/use-segments.ts +188 -0
  44. package/src/browser/request-controller.ts +164 -0
  45. package/src/browser/rsc-router.tsx +353 -0
  46. package/src/browser/scroll-restoration.ts +324 -0
  47. package/src/browser/server-action-bridge.ts +747 -0
  48. package/src/browser/shallow.ts +35 -0
  49. package/src/browser/types.ts +464 -0
  50. package/src/cache/__tests__/document-cache.test.ts +522 -0
  51. package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
  52. package/src/cache/__tests__/memory-store.test.ts +484 -0
  53. package/src/cache/cache-scope.ts +565 -0
  54. package/src/cache/cf/__tests__/cf-cache-store.test.ts +428 -0
  55. package/src/cache/cf/cf-cache-store.ts +428 -0
  56. package/src/cache/cf/index.ts +19 -0
  57. package/src/cache/document-cache.ts +340 -0
  58. package/src/cache/index.ts +58 -0
  59. package/src/cache/memory-segment-store.ts +150 -0
  60. package/src/cache/memory-store.ts +253 -0
  61. package/src/cache/types.ts +387 -0
  62. package/src/client.rsc.tsx +88 -0
  63. package/src/client.tsx +621 -0
  64. package/src/component-utils.ts +76 -0
  65. package/src/components/DefaultDocument.tsx +23 -0
  66. package/src/default-error-boundary.tsx +88 -0
  67. package/src/deps/browser.ts +8 -0
  68. package/src/deps/html-stream-client.ts +2 -0
  69. package/src/deps/html-stream-server.ts +2 -0
  70. package/src/deps/rsc.ts +10 -0
  71. package/src/deps/ssr.ts +2 -0
  72. package/src/errors.ts +259 -0
  73. package/src/handle.ts +120 -0
  74. package/src/handles/MetaTags.tsx +193 -0
  75. package/src/handles/index.ts +6 -0
  76. package/src/handles/meta.ts +247 -0
  77. package/src/href-client.ts +128 -0
  78. package/src/href-context.ts +33 -0
  79. package/src/href.ts +177 -0
  80. package/src/index.rsc.ts +79 -0
  81. package/src/index.ts +87 -0
  82. package/src/loader.rsc.ts +204 -0
  83. package/src/loader.ts +47 -0
  84. package/src/network-error-thrower.tsx +21 -0
  85. package/src/outlet-context.ts +15 -0
  86. package/src/root-error-boundary.tsx +277 -0
  87. package/src/route-content-wrapper.tsx +198 -0
  88. package/src/route-definition.ts +1371 -0
  89. package/src/route-map-builder.ts +146 -0
  90. package/src/route-types.ts +198 -0
  91. package/src/route-utils.ts +89 -0
  92. package/src/router/__tests__/match-context.test.ts +104 -0
  93. package/src/router/__tests__/match-pipelines.test.ts +537 -0
  94. package/src/router/__tests__/match-result.test.ts +566 -0
  95. package/src/router/__tests__/on-error.test.ts +935 -0
  96. package/src/router/__tests__/pattern-matching.test.ts +577 -0
  97. package/src/router/error-handling.ts +287 -0
  98. package/src/router/handler-context.ts +158 -0
  99. package/src/router/loader-resolution.ts +326 -0
  100. package/src/router/manifest.ts +138 -0
  101. package/src/router/match-context.ts +264 -0
  102. package/src/router/match-middleware/background-revalidation.ts +236 -0
  103. package/src/router/match-middleware/cache-lookup.ts +261 -0
  104. package/src/router/match-middleware/cache-store.ts +266 -0
  105. package/src/router/match-middleware/index.ts +81 -0
  106. package/src/router/match-middleware/intercept-resolution.ts +268 -0
  107. package/src/router/match-middleware/segment-resolution.ts +174 -0
  108. package/src/router/match-pipelines.ts +214 -0
  109. package/src/router/match-result.ts +214 -0
  110. package/src/router/metrics.ts +62 -0
  111. package/src/router/middleware.test.ts +1355 -0
  112. package/src/router/middleware.ts +748 -0
  113. package/src/router/pattern-matching.ts +272 -0
  114. package/src/router/revalidation.ts +190 -0
  115. package/src/router/router-context.ts +299 -0
  116. package/src/router/types.ts +96 -0
  117. package/src/router.ts +3876 -0
  118. package/src/rsc/__tests__/helpers.test.ts +175 -0
  119. package/src/rsc/handler.ts +1060 -0
  120. package/src/rsc/helpers.ts +64 -0
  121. package/src/rsc/index.ts +56 -0
  122. package/src/rsc/nonce.ts +18 -0
  123. package/src/rsc/types.ts +237 -0
  124. package/src/segment-system.tsx +456 -0
  125. package/src/server/__tests__/request-context.test.ts +171 -0
  126. package/src/server/context.ts +417 -0
  127. package/src/server/handle-store.ts +230 -0
  128. package/src/server/loader-registry.ts +174 -0
  129. package/src/server/request-context.ts +554 -0
  130. package/src/server/root-layout.tsx +10 -0
  131. package/src/server/tsconfig.json +14 -0
  132. package/src/server.ts +146 -0
  133. package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
  134. package/src/ssr/index.tsx +234 -0
  135. package/src/theme/ThemeProvider.tsx +291 -0
  136. package/src/theme/ThemeScript.tsx +61 -0
  137. package/src/theme/__tests__/theme.test.ts +120 -0
  138. package/src/theme/constants.ts +55 -0
  139. package/src/theme/index.ts +58 -0
  140. package/src/theme/theme-context.ts +70 -0
  141. package/src/theme/theme-script.ts +152 -0
  142. package/src/theme/types.ts +182 -0
  143. package/src/theme/use-theme.ts +44 -0
  144. package/src/types.ts +1561 -0
  145. package/src/urls.ts +726 -0
  146. package/src/use-loader.tsx +346 -0
  147. package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
  148. package/src/vite/expose-action-id.ts +344 -0
  149. package/src/vite/expose-handle-id.ts +209 -0
  150. package/src/vite/expose-loader-id.ts +357 -0
  151. package/src/vite/expose-location-state-id.ts +177 -0
  152. package/src/vite/index.ts +787 -0
  153. package/src/vite/package-resolution.ts +125 -0
  154. package/src/vite/version.d.ts +12 -0
  155. package/src/vite/virtual-entries.ts +109 -0
package/src/server.ts ADDED
@@ -0,0 +1,146 @@
1
+ /**
2
+ * rsc-router/server
3
+ *
4
+ * Server-only exports for route definition and building
5
+ * These should only be imported in server-side handler files
6
+ */
7
+
8
+ // Route definition helpers (server-only)
9
+ export {
10
+ route,
11
+ map,
12
+ createLoader,
13
+ redirect,
14
+ type RouteHelpers,
15
+ type RouteHandlers,
16
+ type InterceptSelectorContext,
17
+ type InterceptSegmentsState,
18
+ type InterceptWhenFn,
19
+ } from "./route-definition.js";
20
+
21
+ // Django-style URL patterns (server-only)
22
+ export {
23
+ urls,
24
+ type PathHelpers,
25
+ type PathOptions,
26
+ type UrlPatterns,
27
+ type IncludeOptions,
28
+ } from "./urls.js";
29
+
30
+ // Re-export IncludeItem from route-types
31
+ export type { IncludeItem } from "./route-types.js";
32
+
33
+ // Core router (server-only)
34
+ export {
35
+ createRSCRouter,
36
+ type RSCRouter,
37
+ type RSCRouterOptions,
38
+ type RootLayoutProps,
39
+ } from "./router.js";
40
+
41
+ // Type-safe href utilities
42
+ export {
43
+ createHref,
44
+ type HrefFunction,
45
+ type PrefixedRoutes,
46
+ type PrefixRoutePatterns,
47
+ type ParamsFor,
48
+ type SanitizePrefix,
49
+ type MergeRoutes,
50
+ } from "./href.js";
51
+
52
+ // Segment system (server-only)
53
+ export { renderSegments } from "./segment-system.js";
54
+
55
+ // Performance tracking (server-only)
56
+ export { track } from "./server/context.js";
57
+
58
+ // Handle API (works in both server and client contexts)
59
+ export { createHandle, isHandle, type Handle } from "./handle.js";
60
+
61
+ // Built-in handles
62
+ export { Meta } from "./handles/meta.js";
63
+
64
+ // Loader registry (for GET-based loader fetching)
65
+ export { registerLoaderById, setLoaderImports } from "./server/loader-registry.js";
66
+
67
+ // Request context (for accessing request data in server components/actions)
68
+ export {
69
+ getRequestContext,
70
+ requireRequestContext,
71
+ createRequestContext,
72
+ type RequestContext,
73
+ type CreateRequestContextOptions,
74
+ } from "./server/request-context.js";
75
+
76
+ // Meta types
77
+ export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
78
+
79
+ // Middleware types
80
+ export type {
81
+ MiddlewareFn,
82
+ MiddlewareFn as AppMiddlewareFn, // Alias for backwards compatibility
83
+ MiddlewareContext,
84
+ CookieOptions,
85
+ } from "./router/middleware.js";
86
+
87
+ // Error classes and utilities
88
+ export {
89
+ RouteNotFoundError,
90
+ DataNotFoundError,
91
+ notFound,
92
+ MiddlewareError,
93
+ HandlerError,
94
+ BuildError,
95
+ InvalidHandlerError,
96
+ sanitizeError,
97
+ } from "./errors.js";
98
+
99
+ // Component utilities
100
+ export {
101
+ isClientComponent,
102
+ assertClientComponent,
103
+ } from "./component-utils.js";
104
+
105
+ // Types (re-exported for convenience)
106
+ export type {
107
+ RouterEnv,
108
+ DefaultEnv,
109
+ RouteDefinition,
110
+ RouteConfig,
111
+ RouteDefinitionOptions,
112
+ TrailingSlashMode,
113
+ ResolvedRouteMap,
114
+ Handler,
115
+ HandlerContext,
116
+ HandlersForRouteMap,
117
+ ResolvedSegment,
118
+ SegmentMetadata,
119
+ MatchResult,
120
+ SlotState,
121
+ ExtractParams,
122
+ GenericParams,
123
+ RevalidateParams,
124
+ ShouldRevalidateFn,
125
+ RouteKeys,
126
+ RouteHandler,
127
+ RouteRevalidateFn,
128
+ RouteMiddlewareFn,
129
+ LoaderDefinition,
130
+ LoaderFn,
131
+ LoaderContext,
132
+ GetRegisteredRoutes,
133
+ // Error boundary types
134
+ ErrorInfo,
135
+ ErrorBoundaryFallbackProps,
136
+ ErrorBoundaryHandler,
137
+ ClientErrorBoundaryFallbackProps,
138
+ // NotFound boundary types
139
+ NotFoundInfo,
140
+ NotFoundBoundaryFallbackProps,
141
+ NotFoundBoundaryHandler,
142
+ // Error handling callback types
143
+ ErrorPhase,
144
+ OnErrorContext,
145
+ OnErrorCallback,
146
+ } from "./types.js";
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import React from "react";
3
+ import { createSSRHandler, type SSRDependencies } from "../index";
4
+
5
+ describe("createSSRHandler", () => {
6
+ // Mock dependencies
7
+ const createMockDeps = (
8
+ overrides: Partial<SSRDependencies> = {}
9
+ ): SSRDependencies => ({
10
+ createFromReadableStream: vi.fn().mockResolvedValue({
11
+ root: React.createElement("div", null, "Test"),
12
+ metadata: { matched: ["/"], pathname: "/" },
13
+ }),
14
+ renderToReadableStream: vi.fn().mockResolvedValue(
15
+ new ReadableStream({
16
+ start(controller) {
17
+ controller.enqueue(new TextEncoder().encode("<html><body>Test</body></html>"));
18
+ controller.close();
19
+ },
20
+ })
21
+ ),
22
+ injectRSCPayload: vi.fn().mockReturnValue(
23
+ new TransformStream({
24
+ transform(chunk, controller) {
25
+ controller.enqueue(chunk);
26
+ },
27
+ })
28
+ ),
29
+ loadBootstrapScriptContent: vi.fn().mockResolvedValue("console.log('bootstrap')"),
30
+ ...overrides,
31
+ });
32
+
33
+ const createMockRscStream = () =>
34
+ new ReadableStream({
35
+ start(controller) {
36
+ controller.enqueue(new TextEncoder().encode("mock rsc payload"));
37
+ controller.close();
38
+ },
39
+ });
40
+
41
+ describe("onError callback", () => {
42
+ it("should call onError when rendering fails", async () => {
43
+ const onError = vi.fn();
44
+ const renderError = new Error("Rendering failed");
45
+
46
+ const deps = createMockDeps({
47
+ renderToReadableStream: vi.fn().mockRejectedValue(renderError),
48
+ onError,
49
+ });
50
+
51
+ const renderHTML = createSSRHandler(deps);
52
+
53
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow("Rendering failed");
54
+
55
+ expect(onError).toHaveBeenCalledTimes(1);
56
+ expect(onError).toHaveBeenCalledWith(renderError, { phase: "rendering" });
57
+ });
58
+
59
+ it("should call onError with phase: rendering", async () => {
60
+ const onError = vi.fn();
61
+
62
+ const deps = createMockDeps({
63
+ // Use loadBootstrapScriptContent error since it happens before React.use()
64
+ loadBootstrapScriptContent: vi.fn().mockRejectedValue(new Error("Bootstrap failed")),
65
+ onError,
66
+ });
67
+
68
+ const renderHTML = createSSRHandler(deps);
69
+
70
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow("Bootstrap failed");
71
+
72
+ expect(onError).toHaveBeenCalledWith(
73
+ expect.any(Error),
74
+ { phase: "rendering" }
75
+ );
76
+ });
77
+
78
+ it("should convert non-Error objects to Error", async () => {
79
+ const onError = vi.fn();
80
+
81
+ const deps = createMockDeps({
82
+ renderToReadableStream: vi.fn().mockRejectedValue("string error"),
83
+ onError,
84
+ });
85
+
86
+ const renderHTML = createSSRHandler(deps);
87
+
88
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow();
89
+
90
+ expect(onError).toHaveBeenCalledWith(
91
+ expect.objectContaining({
92
+ message: "string error",
93
+ }),
94
+ { phase: "rendering" }
95
+ );
96
+ });
97
+
98
+ it("should still throw original error after calling onError", async () => {
99
+ const onError = vi.fn();
100
+ const originalError = new Error("Original error");
101
+
102
+ const deps = createMockDeps({
103
+ loadBootstrapScriptContent: vi.fn().mockRejectedValue(originalError),
104
+ onError,
105
+ });
106
+
107
+ const renderHTML = createSSRHandler(deps);
108
+
109
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow("Original error");
110
+
111
+ // Verify onError was called before error was thrown
112
+ expect(onError).toHaveBeenCalled();
113
+ });
114
+
115
+ it("should catch errors in onError callback and not break the flow", async () => {
116
+ const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
117
+ const callbackError = new Error("Callback exploded");
118
+ const onError = vi.fn().mockImplementation(() => {
119
+ throw callbackError;
120
+ });
121
+ const originalError = new Error("Original rendering error");
122
+
123
+ const deps = createMockDeps({
124
+ renderToReadableStream: vi.fn().mockRejectedValue(originalError),
125
+ onError,
126
+ });
127
+
128
+ const renderHTML = createSSRHandler(deps);
129
+
130
+ // Should throw original error, not callback error
131
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow("Original rendering error");
132
+
133
+ expect(onError).toHaveBeenCalled();
134
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
135
+ "[SSRHandler.onError] Callback error:",
136
+ callbackError
137
+ );
138
+
139
+ consoleErrorSpy.mockRestore();
140
+ });
141
+
142
+ it("should not call onError when rendering succeeds", async () => {
143
+ const onError = vi.fn();
144
+
145
+ const deps = createMockDeps({ onError });
146
+ const renderHTML = createSSRHandler(deps);
147
+
148
+ const result = await renderHTML(createMockRscStream());
149
+
150
+ expect(result).toBeInstanceOf(ReadableStream);
151
+ expect(onError).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it("should work without onError callback", async () => {
155
+ const deps = createMockDeps({
156
+ renderToReadableStream: vi.fn().mockRejectedValue(new Error("No callback test")),
157
+ onError: undefined,
158
+ });
159
+
160
+ const renderHTML = createSSRHandler(deps);
161
+
162
+ // Should not throw because of missing onError
163
+ await expect(renderHTML(createMockRscStream())).rejects.toThrow("No callback test");
164
+ });
165
+ });
166
+
167
+ describe("successful rendering", () => {
168
+ it("should return a ReadableStream on success", async () => {
169
+ const deps = createMockDeps();
170
+ const renderHTML = createSSRHandler(deps);
171
+
172
+ const result = await renderHTML(createMockRscStream());
173
+
174
+ expect(result).toBeInstanceOf(ReadableStream);
175
+ });
176
+
177
+ it("should call all dependencies in correct order", async () => {
178
+ const deps = createMockDeps();
179
+ const renderHTML = createSSRHandler(deps);
180
+
181
+ await renderHTML(createMockRscStream());
182
+
183
+ expect(deps.loadBootstrapScriptContent).toHaveBeenCalled();
184
+ expect(deps.renderToReadableStream).toHaveBeenCalled();
185
+ expect(deps.injectRSCPayload).toHaveBeenCalled();
186
+ });
187
+ });
188
+ });
@@ -0,0 +1,234 @@
1
+ import React from "react";
2
+ import { initHandleDataSync } from "../browser/react/use-handle.js";
3
+ import { initSegmentsSync } from "../browser/react/use-segments.js";
4
+ import { initThemeConfigSync } from "../theme/theme-context.js";
5
+ import { ThemeProvider } from "../theme/ThemeProvider.js";
6
+ import type { HandleData } from "../browser/types.js";
7
+ import type { ErrorPhase } from "../types.js";
8
+ import type { ResolvedThemeConfig, Theme } from "../theme/types.js";
9
+
10
+ /**
11
+ * Options for injectRSCPayload
12
+ */
13
+ export interface InjectRSCPayloadOptions {
14
+ /**
15
+ * Nonce for Content Security Policy (CSP)
16
+ */
17
+ nonce?: string;
18
+ }
19
+
20
+ /**
21
+ * Options for renderToReadableStream from react-dom/server
22
+ */
23
+ interface RenderToReadableStreamOptions {
24
+ bootstrapScriptContent?: string;
25
+ nonce?: string;
26
+ formState?: unknown;
27
+ }
28
+
29
+ /**
30
+ * Options for the renderHTML function
31
+ */
32
+ export interface SSRRenderOptions {
33
+ /**
34
+ * Form state for useActionState progressive enhancement.
35
+ * This is the result of decodeFormState() and should be passed to
36
+ * react-dom's renderToReadableStream to enable useActionState to
37
+ * receive the action result during SSR.
38
+ */
39
+ formState?: unknown;
40
+
41
+ /**
42
+ * Nonce for Content Security Policy (CSP)
43
+ */
44
+ nonce?: string;
45
+ }
46
+
47
+ /**
48
+ * SSR dependencies from external packages
49
+ */
50
+ export interface SSRDependencies<TEnv = unknown> {
51
+ /**
52
+ * createFromReadableStream from @vitejs/plugin-rsc/ssr
53
+ */
54
+ createFromReadableStream: <T>(stream: ReadableStream<Uint8Array>) => Promise<T>;
55
+
56
+ /**
57
+ * renderToReadableStream from react-dom/server.edge
58
+ */
59
+ renderToReadableStream: (
60
+ element: React.ReactNode,
61
+ options?: RenderToReadableStreamOptions
62
+ ) => Promise<ReadableStream<Uint8Array>>;
63
+
64
+ /**
65
+ * injectRSCPayload from rsc-html-stream/server
66
+ */
67
+ injectRSCPayload: (
68
+ rscStream: ReadableStream<Uint8Array>,
69
+ options?: InjectRSCPayloadOptions
70
+ ) => TransformStream<Uint8Array, Uint8Array>;
71
+
72
+ /**
73
+ * Function to load bootstrap script content
74
+ * Typically: () => import.meta.viteRsc.loadBootstrapScriptContent("index")
75
+ */
76
+ loadBootstrapScriptContent: () => Promise<string>;
77
+
78
+ /**
79
+ * Optional callback invoked when an error occurs during SSR rendering.
80
+ *
81
+ * This callback is for notification/logging purposes.
82
+ *
83
+ * @example
84
+ * ```typescript
85
+ * export const renderHTML = createSSRHandler({
86
+ * // ... other deps
87
+ * onError: (error, context) => {
88
+ * console.error('[SSR] Rendering error:', error);
89
+ * Sentry.captureException(error);
90
+ * },
91
+ * });
92
+ * ```
93
+ */
94
+ onError?: (error: Error, context: { phase: ErrorPhase }) => void;
95
+ }
96
+
97
+ /**
98
+ * RSC payload type (minimal interface for SSR)
99
+ */
100
+ interface RscPayload {
101
+ root: React.ReactNode;
102
+ metadata?: {
103
+ handles?: AsyncGenerator<HandleData, void, unknown>;
104
+ matched?: string[];
105
+ pathname?: string;
106
+ themeConfig?: ResolvedThemeConfig | null;
107
+ initialTheme?: Theme;
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Consume an async generator and return a Promise that resolves with the final value.
113
+ * Used for SSR where we need to await all handle data before rendering.
114
+ */
115
+ async function consumeAsyncGenerator(
116
+ generator: AsyncGenerator<HandleData, void, unknown>
117
+ ): Promise<HandleData> {
118
+ let lastData: HandleData = {};
119
+ for await (const data of generator) {
120
+ lastData = data;
121
+ }
122
+ return lastData;
123
+ }
124
+
125
+ /**
126
+ * Create an SSR handler that converts RSC streams to HTML.
127
+ *
128
+ * @example
129
+ * ```tsx
130
+ * import { createSSRHandler } from "rsc-router/ssr";
131
+ * import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
132
+ * import { renderToReadableStream } from "react-dom/server.edge";
133
+ * import { injectRSCPayload } from "rsc-html-stream/server";
134
+ *
135
+ * export const renderHTML = createSSRHandler({
136
+ * createFromReadableStream,
137
+ * renderToReadableStream,
138
+ * injectRSCPayload,
139
+ * loadBootstrapScriptContent: () =>
140
+ * import.meta.viteRsc.loadBootstrapScriptContent("index"),
141
+ * });
142
+ * ```
143
+ */
144
+ export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
145
+ const {
146
+ createFromReadableStream,
147
+ renderToReadableStream,
148
+ injectRSCPayload,
149
+ loadBootstrapScriptContent,
150
+ onError,
151
+ } = deps;
152
+
153
+ /**
154
+ * Render RSC stream to HTML stream
155
+ *
156
+ * @param rscStream - The RSC stream to render
157
+ * @param options - Optional render options including formState for useActionState and nonce for CSP
158
+ */
159
+ return async function renderHTML(
160
+ rscStream: ReadableStream<Uint8Array>,
161
+ options?: SSRRenderOptions
162
+ ): Promise<ReadableStream<Uint8Array>> {
163
+ const { nonce, formState } = options ?? {};
164
+
165
+ try {
166
+ // Tee the stream:
167
+ // - rscStream1: For SSR rendering (deserialize to React VDOM)
168
+ // - rscStream2: For browser hydration (inject as __FLIGHT_DATA__)
169
+ const [rscStream1, rscStream2] = rscStream.tee();
170
+
171
+ // Deserialize RSC stream to React tree
172
+ let payload: Promise<RscPayload> | undefined;
173
+ let handlesPromise: Promise<HandleData> | undefined;
174
+ function SsrRoot() {
175
+ payload ??= createFromReadableStream<RscPayload>(rscStream1);
176
+ const resolved = React.use(payload);
177
+
178
+ // Initialize segments state before children render (for useSegments hook)
179
+ initSegmentsSync(resolved.metadata?.matched, resolved.metadata?.pathname);
180
+
181
+ // Initialize theme config for MetaTags to render theme script
182
+ const themeConfig = resolved.metadata?.themeConfig ?? null;
183
+ initThemeConfigSync(themeConfig);
184
+
185
+ // Await handles and initialize state before children render
186
+ // The handles property is an async generator that yields on each push
187
+ // Memoize the promise since async generators can only be iterated once
188
+ if (resolved.metadata?.handles) {
189
+ handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
190
+ const handleData = React.use(handlesPromise);
191
+ initHandleDataSync(handleData, resolved.metadata.matched);
192
+ }
193
+
194
+ // Wrap content with ThemeProvider if theme is enabled
195
+ // This provides the theme context for client components that use useTheme
196
+ if (themeConfig) {
197
+ return (
198
+ <ThemeProvider config={themeConfig} initialTheme={resolved.metadata?.initialTheme}>
199
+ {resolved.root}
200
+ </ThemeProvider>
201
+ );
202
+ }
203
+
204
+ return resolved.root;
205
+ }
206
+
207
+ // Get bootstrap script content
208
+ const bootstrapScriptContent = await loadBootstrapScriptContent();
209
+
210
+ // Render React tree to HTML stream
211
+ // Pass formState for useActionState progressive enhancement if provided
212
+ // Pass nonce for CSP if provided
213
+ const htmlStream = await renderToReadableStream(<SsrRoot />, {
214
+ bootstrapScriptContent,
215
+ formState,
216
+ nonce,
217
+ });
218
+
219
+ // Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
220
+ return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
221
+ } catch (error) {
222
+ // Invoke onError callback if provided
223
+ if (onError) {
224
+ const errorObj = error instanceof Error ? error : new Error(String(error));
225
+ try {
226
+ onError(errorObj, { phase: "rendering" });
227
+ } catch (callbackError) {
228
+ console.error("[SSRHandler.onError] Callback error:", callbackError);
229
+ }
230
+ }
231
+ throw error;
232
+ }
233
+ };
234
+ }