@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.
- package/README.md +19 -0
- package/package.json +131 -0
- package/src/__mocks__/version.ts +6 -0
- package/src/__tests__/route-definition.test.ts +63 -0
- package/src/browser/event-controller.ts +876 -0
- package/src/browser/index.ts +18 -0
- package/src/browser/link-interceptor.ts +121 -0
- package/src/browser/lru-cache.ts +69 -0
- package/src/browser/merge-segment-loaders.ts +126 -0
- package/src/browser/navigation-bridge.ts +891 -0
- package/src/browser/navigation-client.ts +155 -0
- package/src/browser/navigation-store.ts +823 -0
- package/src/browser/partial-update.ts +545 -0
- package/src/browser/react/Link.tsx +248 -0
- package/src/browser/react/NavigationProvider.tsx +228 -0
- package/src/browser/react/ScrollRestoration.tsx +94 -0
- package/src/browser/react/context.ts +53 -0
- package/src/browser/react/index.ts +52 -0
- package/src/browser/react/location-state-shared.ts +120 -0
- package/src/browser/react/location-state.ts +62 -0
- package/src/browser/react/use-action.ts +240 -0
- package/src/browser/react/use-client-cache.ts +56 -0
- package/src/browser/react/use-handle.ts +178 -0
- package/src/browser/react/use-link-status.ts +134 -0
- package/src/browser/react/use-navigation.ts +150 -0
- package/src/browser/react/use-segments.ts +188 -0
- package/src/browser/request-controller.ts +149 -0
- package/src/browser/rsc-router.tsx +310 -0
- package/src/browser/scroll-restoration.ts +324 -0
- package/src/browser/server-action-bridge.ts +747 -0
- package/src/browser/shallow.ts +35 -0
- package/src/browser/types.ts +443 -0
- package/src/cache/__tests__/memory-segment-store.test.ts +487 -0
- package/src/cache/__tests__/memory-store.test.ts +484 -0
- package/src/cache/cache-scope.ts +565 -0
- package/src/cache/cf/__tests__/cf-cache-store.test.ts +361 -0
- package/src/cache/cf/cf-cache-store.ts +274 -0
- package/src/cache/cf/index.ts +19 -0
- package/src/cache/index.ts +52 -0
- package/src/cache/memory-segment-store.ts +150 -0
- package/src/cache/memory-store.ts +253 -0
- package/src/cache/types.ts +366 -0
- package/src/client.rsc.tsx +88 -0
- package/src/client.tsx +609 -0
- package/src/components/DefaultDocument.tsx +20 -0
- package/src/default-error-boundary.tsx +88 -0
- package/src/deps/browser.ts +8 -0
- package/src/deps/html-stream-client.ts +2 -0
- package/src/deps/html-stream-server.ts +2 -0
- package/src/deps/rsc.ts +10 -0
- package/src/deps/ssr.ts +2 -0
- package/src/errors.ts +259 -0
- package/src/handle.ts +120 -0
- package/src/handles/MetaTags.tsx +178 -0
- package/src/handles/index.ts +6 -0
- package/src/handles/meta.ts +247 -0
- package/src/href-client.ts +128 -0
- package/src/href.ts +139 -0
- package/src/index.rsc.ts +69 -0
- package/src/index.ts +84 -0
- package/src/loader.rsc.ts +204 -0
- package/src/loader.ts +47 -0
- package/src/network-error-thrower.tsx +21 -0
- package/src/outlet-context.ts +15 -0
- package/src/root-error-boundary.tsx +277 -0
- package/src/route-content-wrapper.tsx +198 -0
- package/src/route-definition.ts +1333 -0
- package/src/route-map-builder.ts +140 -0
- package/src/route-types.ts +148 -0
- package/src/route-utils.ts +89 -0
- package/src/router/__tests__/match-context.test.ts +104 -0
- package/src/router/__tests__/match-pipelines.test.ts +537 -0
- package/src/router/__tests__/match-result.test.ts +566 -0
- package/src/router/__tests__/on-error.test.ts +935 -0
- package/src/router/__tests__/pattern-matching.test.ts +577 -0
- package/src/router/error-handling.ts +287 -0
- package/src/router/handler-context.ts +60 -0
- package/src/router/loader-resolution.ts +326 -0
- package/src/router/manifest.ts +116 -0
- package/src/router/match-context.ts +261 -0
- package/src/router/match-middleware/background-revalidation.ts +236 -0
- package/src/router/match-middleware/cache-lookup.ts +261 -0
- package/src/router/match-middleware/cache-store.ts +250 -0
- package/src/router/match-middleware/index.ts +81 -0
- package/src/router/match-middleware/intercept-resolution.ts +268 -0
- package/src/router/match-middleware/segment-resolution.ts +174 -0
- package/src/router/match-pipelines.ts +214 -0
- package/src/router/match-result.ts +212 -0
- package/src/router/metrics.ts +62 -0
- package/src/router/middleware.test.ts +1355 -0
- package/src/router/middleware.ts +748 -0
- package/src/router/pattern-matching.ts +271 -0
- package/src/router/revalidation.ts +190 -0
- package/src/router/router-context.ts +299 -0
- package/src/router/types.ts +96 -0
- package/src/router.ts +3484 -0
- package/src/rsc/__tests__/helpers.test.ts +175 -0
- package/src/rsc/handler.ts +942 -0
- package/src/rsc/helpers.ts +64 -0
- package/src/rsc/index.ts +56 -0
- package/src/rsc/nonce.ts +18 -0
- package/src/rsc/types.ts +225 -0
- package/src/segment-system.tsx +405 -0
- package/src/server/__tests__/request-context.test.ts +171 -0
- package/src/server/context.ts +340 -0
- package/src/server/handle-store.ts +230 -0
- package/src/server/loader-registry.ts +174 -0
- package/src/server/request-context.ts +470 -0
- package/src/server/root-layout.tsx +10 -0
- package/src/server/tsconfig.json +14 -0
- package/src/server.ts +126 -0
- package/src/ssr/__tests__/ssr-handler.test.tsx +188 -0
- package/src/ssr/index.tsx +215 -0
- package/src/types.ts +1473 -0
- package/src/use-loader.tsx +346 -0
- package/src/vite/__tests__/expose-loader-id.test.ts +117 -0
- package/src/vite/expose-action-id.ts +344 -0
- package/src/vite/expose-handle-id.ts +209 -0
- package/src/vite/expose-loader-id.ts +357 -0
- package/src/vite/expose-location-state-id.ts +177 -0
- package/src/vite/index.ts +608 -0
- package/src/vite/version.d.ts +12 -0
- package/src/vite/virtual-entries.ts +109 -0
package/src/server.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
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 InterceptSelectorContext,
|
|
16
|
+
type InterceptSegmentsState,
|
|
17
|
+
type InterceptWhenFn,
|
|
18
|
+
} from "./route-definition.js";
|
|
19
|
+
|
|
20
|
+
// Core router (server-only)
|
|
21
|
+
export {
|
|
22
|
+
createRSCRouter,
|
|
23
|
+
type RSCRouter,
|
|
24
|
+
type RSCRouterOptions,
|
|
25
|
+
type RootLayoutProps,
|
|
26
|
+
} from "./router.js";
|
|
27
|
+
|
|
28
|
+
// Type-safe href utilities
|
|
29
|
+
export {
|
|
30
|
+
createHref,
|
|
31
|
+
type HrefFunction,
|
|
32
|
+
type PrefixedRoutes,
|
|
33
|
+
type ParamsFor,
|
|
34
|
+
type SanitizePrefix,
|
|
35
|
+
type MergeRoutes,
|
|
36
|
+
} from "./href.js";
|
|
37
|
+
|
|
38
|
+
// Segment system (server-only)
|
|
39
|
+
export { renderSegments } from "./segment-system.js";
|
|
40
|
+
|
|
41
|
+
// Performance tracking (server-only)
|
|
42
|
+
export { track } from "./server/context.js";
|
|
43
|
+
|
|
44
|
+
// Handle API (works in both server and client contexts)
|
|
45
|
+
export { createHandle, isHandle, type Handle } from "./handle.js";
|
|
46
|
+
|
|
47
|
+
// Built-in handles
|
|
48
|
+
export { Meta } from "./handles/meta.js";
|
|
49
|
+
|
|
50
|
+
// Loader registry (for GET-based loader fetching)
|
|
51
|
+
export { registerLoaderById, setLoaderImports } from "./server/loader-registry.js";
|
|
52
|
+
|
|
53
|
+
// Request context (for accessing request data in server components/actions)
|
|
54
|
+
export {
|
|
55
|
+
getRequestContext,
|
|
56
|
+
requireRequestContext,
|
|
57
|
+
createRequestContext,
|
|
58
|
+
type RequestContext,
|
|
59
|
+
type CreateRequestContextOptions,
|
|
60
|
+
} from "./server/request-context.js";
|
|
61
|
+
|
|
62
|
+
// Meta types
|
|
63
|
+
export type { MetaDescriptor, MetaDescriptorBase } from "./router/types.js";
|
|
64
|
+
|
|
65
|
+
// Middleware types
|
|
66
|
+
export type {
|
|
67
|
+
MiddlewareFn,
|
|
68
|
+
MiddlewareFn as AppMiddlewareFn, // Alias for backwards compatibility
|
|
69
|
+
MiddlewareContext,
|
|
70
|
+
CookieOptions,
|
|
71
|
+
} from "./router/middleware.js";
|
|
72
|
+
|
|
73
|
+
// Error classes and utilities
|
|
74
|
+
export {
|
|
75
|
+
RouteNotFoundError,
|
|
76
|
+
DataNotFoundError,
|
|
77
|
+
notFound,
|
|
78
|
+
MiddlewareError,
|
|
79
|
+
HandlerError,
|
|
80
|
+
BuildError,
|
|
81
|
+
InvalidHandlerError,
|
|
82
|
+
sanitizeError,
|
|
83
|
+
} from "./errors.js";
|
|
84
|
+
|
|
85
|
+
// Types (re-exported for convenience)
|
|
86
|
+
export type {
|
|
87
|
+
RouterEnv,
|
|
88
|
+
DefaultEnv,
|
|
89
|
+
RouteDefinition,
|
|
90
|
+
RouteConfig,
|
|
91
|
+
RouteDefinitionOptions,
|
|
92
|
+
TrailingSlashMode,
|
|
93
|
+
ResolvedRouteMap,
|
|
94
|
+
Handler,
|
|
95
|
+
HandlerContext,
|
|
96
|
+
HandlersForRouteMap,
|
|
97
|
+
ResolvedSegment,
|
|
98
|
+
SegmentMetadata,
|
|
99
|
+
MatchResult,
|
|
100
|
+
SlotState,
|
|
101
|
+
ExtractParams,
|
|
102
|
+
GenericParams,
|
|
103
|
+
RevalidateParams,
|
|
104
|
+
ShouldRevalidateFn,
|
|
105
|
+
RouteKeys,
|
|
106
|
+
RouteHandler,
|
|
107
|
+
RouteRevalidateFn,
|
|
108
|
+
RouteMiddlewareFn,
|
|
109
|
+
LoaderDefinition,
|
|
110
|
+
LoaderFn,
|
|
111
|
+
LoaderContext,
|
|
112
|
+
GetRegisteredRoutes,
|
|
113
|
+
// Error boundary types
|
|
114
|
+
ErrorInfo,
|
|
115
|
+
ErrorBoundaryFallbackProps,
|
|
116
|
+
ErrorBoundaryHandler,
|
|
117
|
+
ClientErrorBoundaryFallbackProps,
|
|
118
|
+
// NotFound boundary types
|
|
119
|
+
NotFoundInfo,
|
|
120
|
+
NotFoundBoundaryFallbackProps,
|
|
121
|
+
NotFoundBoundaryHandler,
|
|
122
|
+
// Error handling callback types
|
|
123
|
+
ErrorPhase,
|
|
124
|
+
OnErrorContext,
|
|
125
|
+
OnErrorCallback,
|
|
126
|
+
} 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,215 @@
|
|
|
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 type { HandleData } from "../browser/types.js";
|
|
5
|
+
import type { ErrorPhase } from "../types.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Options for injectRSCPayload
|
|
9
|
+
*/
|
|
10
|
+
export interface InjectRSCPayloadOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Nonce for Content Security Policy (CSP)
|
|
13
|
+
*/
|
|
14
|
+
nonce?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Options for renderToReadableStream from react-dom/server
|
|
19
|
+
*/
|
|
20
|
+
interface RenderToReadableStreamOptions {
|
|
21
|
+
bootstrapScriptContent?: string;
|
|
22
|
+
nonce?: string;
|
|
23
|
+
formState?: unknown;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Options for the renderHTML function
|
|
28
|
+
*/
|
|
29
|
+
export interface SSRRenderOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Form state for useActionState progressive enhancement.
|
|
32
|
+
* This is the result of decodeFormState() and should be passed to
|
|
33
|
+
* react-dom's renderToReadableStream to enable useActionState to
|
|
34
|
+
* receive the action result during SSR.
|
|
35
|
+
*/
|
|
36
|
+
formState?: unknown;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Nonce for Content Security Policy (CSP)
|
|
40
|
+
*/
|
|
41
|
+
nonce?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* SSR dependencies from external packages
|
|
46
|
+
*/
|
|
47
|
+
export interface SSRDependencies<TEnv = unknown> {
|
|
48
|
+
/**
|
|
49
|
+
* createFromReadableStream from @vitejs/plugin-rsc/ssr
|
|
50
|
+
*/
|
|
51
|
+
createFromReadableStream: <T>(stream: ReadableStream<Uint8Array>) => Promise<T>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* renderToReadableStream from react-dom/server.edge
|
|
55
|
+
*/
|
|
56
|
+
renderToReadableStream: (
|
|
57
|
+
element: React.ReactNode,
|
|
58
|
+
options?: RenderToReadableStreamOptions
|
|
59
|
+
) => Promise<ReadableStream<Uint8Array>>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* injectRSCPayload from rsc-html-stream/server
|
|
63
|
+
*/
|
|
64
|
+
injectRSCPayload: (
|
|
65
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
66
|
+
options?: InjectRSCPayloadOptions
|
|
67
|
+
) => TransformStream<Uint8Array, Uint8Array>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Function to load bootstrap script content
|
|
71
|
+
* Typically: () => import.meta.viteRsc.loadBootstrapScriptContent("index")
|
|
72
|
+
*/
|
|
73
|
+
loadBootstrapScriptContent: () => Promise<string>;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Optional callback invoked when an error occurs during SSR rendering.
|
|
77
|
+
*
|
|
78
|
+
* This callback is for notification/logging purposes.
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* ```typescript
|
|
82
|
+
* export const renderHTML = createSSRHandler({
|
|
83
|
+
* // ... other deps
|
|
84
|
+
* onError: (error, context) => {
|
|
85
|
+
* console.error('[SSR] Rendering error:', error);
|
|
86
|
+
* Sentry.captureException(error);
|
|
87
|
+
* },
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
onError?: (error: Error, context: { phase: ErrorPhase }) => void;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* RSC payload type (minimal interface for SSR)
|
|
96
|
+
*/
|
|
97
|
+
interface RscPayload {
|
|
98
|
+
root: React.ReactNode;
|
|
99
|
+
metadata?: {
|
|
100
|
+
handles?: AsyncGenerator<HandleData, void, unknown>;
|
|
101
|
+
matched?: string[];
|
|
102
|
+
pathname?: string;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Consume an async generator and return a Promise that resolves with the final value.
|
|
108
|
+
* Used for SSR where we need to await all handle data before rendering.
|
|
109
|
+
*/
|
|
110
|
+
async function consumeAsyncGenerator(
|
|
111
|
+
generator: AsyncGenerator<HandleData, void, unknown>
|
|
112
|
+
): Promise<HandleData> {
|
|
113
|
+
let lastData: HandleData = {};
|
|
114
|
+
for await (const data of generator) {
|
|
115
|
+
lastData = data;
|
|
116
|
+
}
|
|
117
|
+
return lastData;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create an SSR handler that converts RSC streams to HTML.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```tsx
|
|
125
|
+
* import { createSSRHandler } from "rsc-router/ssr";
|
|
126
|
+
* import { createFromReadableStream } from "@vitejs/plugin-rsc/ssr";
|
|
127
|
+
* import { renderToReadableStream } from "react-dom/server.edge";
|
|
128
|
+
* import { injectRSCPayload } from "rsc-html-stream/server";
|
|
129
|
+
*
|
|
130
|
+
* export const renderHTML = createSSRHandler({
|
|
131
|
+
* createFromReadableStream,
|
|
132
|
+
* renderToReadableStream,
|
|
133
|
+
* injectRSCPayload,
|
|
134
|
+
* loadBootstrapScriptContent: () =>
|
|
135
|
+
* import.meta.viteRsc.loadBootstrapScriptContent("index"),
|
|
136
|
+
* });
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
export function createSSRHandler<TEnv = unknown>(deps: SSRDependencies<TEnv>) {
|
|
140
|
+
const {
|
|
141
|
+
createFromReadableStream,
|
|
142
|
+
renderToReadableStream,
|
|
143
|
+
injectRSCPayload,
|
|
144
|
+
loadBootstrapScriptContent,
|
|
145
|
+
onError,
|
|
146
|
+
} = deps;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Render RSC stream to HTML stream
|
|
150
|
+
*
|
|
151
|
+
* @param rscStream - The RSC stream to render
|
|
152
|
+
* @param options - Optional render options including formState for useActionState and nonce for CSP
|
|
153
|
+
*/
|
|
154
|
+
return async function renderHTML(
|
|
155
|
+
rscStream: ReadableStream<Uint8Array>,
|
|
156
|
+
options?: SSRRenderOptions
|
|
157
|
+
): Promise<ReadableStream<Uint8Array>> {
|
|
158
|
+
const { nonce, formState } = options ?? {};
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
// Tee the stream:
|
|
162
|
+
// - rscStream1: For SSR rendering (deserialize to React VDOM)
|
|
163
|
+
// - rscStream2: For browser hydration (inject as __FLIGHT_DATA__)
|
|
164
|
+
const [rscStream1, rscStream2] = rscStream.tee();
|
|
165
|
+
|
|
166
|
+
// Deserialize RSC stream to React tree
|
|
167
|
+
let payload: Promise<RscPayload> | undefined;
|
|
168
|
+
let handlesPromise: Promise<HandleData> | undefined;
|
|
169
|
+
function SsrRoot() {
|
|
170
|
+
payload ??= createFromReadableStream<RscPayload>(rscStream1);
|
|
171
|
+
const resolved = React.use(payload);
|
|
172
|
+
|
|
173
|
+
// Initialize segments state before children render (for useSegments hook)
|
|
174
|
+
initSegmentsSync(resolved.metadata?.matched, resolved.metadata?.pathname);
|
|
175
|
+
|
|
176
|
+
// Await handles and initialize state before children render
|
|
177
|
+
// The handles property is an async generator that yields on each push
|
|
178
|
+
// Memoize the promise since async generators can only be iterated once
|
|
179
|
+
if (resolved.metadata?.handles) {
|
|
180
|
+
handlesPromise ??= consumeAsyncGenerator(resolved.metadata.handles);
|
|
181
|
+
const handleData = React.use(handlesPromise);
|
|
182
|
+
initHandleDataSync(handleData, resolved.metadata.matched);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return resolved.root;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Get bootstrap script content
|
|
189
|
+
const bootstrapScriptContent = await loadBootstrapScriptContent();
|
|
190
|
+
|
|
191
|
+
// Render React tree to HTML stream
|
|
192
|
+
// Pass formState for useActionState progressive enhancement if provided
|
|
193
|
+
// Pass nonce for CSP if provided
|
|
194
|
+
const htmlStream = await renderToReadableStream(<SsrRoot />, {
|
|
195
|
+
bootstrapScriptContent,
|
|
196
|
+
formState,
|
|
197
|
+
nonce,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Inject RSC payload into HTML as <script nonce="...">__FLIGHT_DATA__</script>
|
|
201
|
+
return htmlStream.pipeThrough(injectRSCPayload(rscStream2, { nonce }));
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// Invoke onError callback if provided
|
|
204
|
+
if (onError) {
|
|
205
|
+
const errorObj = error instanceof Error ? error : new Error(String(error));
|
|
206
|
+
try {
|
|
207
|
+
onError(errorObj, { phase: "rendering" });
|
|
208
|
+
} catch (callbackError) {
|
|
209
|
+
console.error("[SSRHandler.onError] Callback error:", callbackError);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
}
|