rwsdk 0.2.0-alpha.0 → 0.2.0-alpha.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.
- package/dist/runtime/client.js +3 -1
- package/dist/runtime/clientNavigation.js +13 -7
- package/dist/runtime/clientNavigation.test.js +2 -3
- package/dist/runtime/lib/router.d.ts +3 -1
- package/dist/runtime/lib/router.js +33 -4
- package/dist/runtime/requestInfo/types.d.ts +2 -0
- package/dist/runtime/requestInfo/worker.js +1 -1
- package/dist/runtime/worker.js +28 -16
- package/package.json +1 -1
package/dist/runtime/client.js
CHANGED
|
@@ -77,7 +77,9 @@ export const initClient = async ({ transport = fetchTransport, hydrateRootOption
|
|
|
77
77
|
const [streamData, setStreamData] = React.useState(rscPayload);
|
|
78
78
|
const [_isPending, startTransition] = React.useTransition();
|
|
79
79
|
transportContext.setRscPayload = (v) => startTransition(() => setStreamData(v));
|
|
80
|
-
return _jsx(_Fragment, { children:
|
|
80
|
+
return (_jsx(_Fragment, { children: streamData
|
|
81
|
+
? React.use(streamData).node
|
|
82
|
+
: null }));
|
|
81
83
|
}
|
|
82
84
|
hydrateRoot(rootEl, _jsx(Content, {}), {
|
|
83
85
|
onUncaughtError: (error, { componentStack }) => {
|
|
@@ -14,6 +14,9 @@ export function validateClickEvent(event, target) {
|
|
|
14
14
|
if (!href) {
|
|
15
15
|
return false;
|
|
16
16
|
}
|
|
17
|
+
if (href.includes("#")) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
17
20
|
// Skip if target="_blank" or similar
|
|
18
21
|
if (link.target && link.target !== "_self") {
|
|
19
22
|
return false;
|
|
@@ -35,12 +38,12 @@ export function initClientNavigation(opts = {}) {
|
|
|
35
38
|
await globalThis.__rsc_callServer();
|
|
36
39
|
},
|
|
37
40
|
scrollToTop: true,
|
|
38
|
-
scrollBehavior:
|
|
41
|
+
scrollBehavior: "instant",
|
|
39
42
|
...opts,
|
|
40
43
|
};
|
|
41
44
|
// Prevent browser's automatic scroll restoration for popstate
|
|
42
|
-
if (
|
|
43
|
-
history.scrollRestoration =
|
|
45
|
+
if ("scrollRestoration" in history) {
|
|
46
|
+
history.scrollRestoration = "manual";
|
|
44
47
|
}
|
|
45
48
|
// Set up scroll behavior management
|
|
46
49
|
let popStateWasCalled = false;
|
|
@@ -51,7 +54,7 @@ export function initClientNavigation(opts = {}) {
|
|
|
51
54
|
window.scrollTo({
|
|
52
55
|
top: savedScrollPosition.y,
|
|
53
56
|
left: savedScrollPosition.x,
|
|
54
|
-
behavior:
|
|
57
|
+
behavior: "instant",
|
|
55
58
|
});
|
|
56
59
|
savedScrollPosition = null;
|
|
57
60
|
}
|
|
@@ -68,7 +71,7 @@ export function initClientNavigation(opts = {}) {
|
|
|
68
71
|
window.history.replaceState({
|
|
69
72
|
...window.history.state,
|
|
70
73
|
scrollX: 0,
|
|
71
|
-
scrollY: 0
|
|
74
|
+
scrollY: 0,
|
|
72
75
|
}, "", window.location.href);
|
|
73
76
|
}
|
|
74
77
|
popStateWasCalled = false;
|
|
@@ -77,7 +80,10 @@ export function initClientNavigation(opts = {}) {
|
|
|
77
80
|
popStateWasCalled = true;
|
|
78
81
|
// Save the scroll position that the browser would have restored to
|
|
79
82
|
const state = event.state;
|
|
80
|
-
if (state &&
|
|
83
|
+
if (state &&
|
|
84
|
+
typeof state === "object" &&
|
|
85
|
+
"scrollX" in state &&
|
|
86
|
+
"scrollY" in state) {
|
|
81
87
|
savedScrollPosition = { x: state.scrollX, y: state.scrollY };
|
|
82
88
|
}
|
|
83
89
|
else {
|
|
@@ -105,7 +111,7 @@ export function initClientNavigation(opts = {}) {
|
|
|
105
111
|
window.history.replaceState({
|
|
106
112
|
path: window.location.pathname,
|
|
107
113
|
scrollX: window.scrollX,
|
|
108
|
-
scrollY: window.scrollY
|
|
114
|
+
scrollY: window.scrollY,
|
|
109
115
|
}, "", window.location.href);
|
|
110
116
|
window.history.pushState({ path: href }, "", window.location.origin + href);
|
|
111
117
|
await options.onNavigate();
|
|
@@ -35,11 +35,10 @@ describe("clientNavigation", () => {
|
|
|
35
35
|
closest: () => ({ getAttribute: () => undefined }),
|
|
36
36
|
})).toBe(false);
|
|
37
37
|
});
|
|
38
|
-
it("should not
|
|
38
|
+
it("should not include an #hash", () => {
|
|
39
39
|
expect(validateClickEvent(mockEvent, {
|
|
40
40
|
closest: () => ({
|
|
41
|
-
|
|
42
|
-
getAttribute: () => "/test",
|
|
41
|
+
getAttribute: () => "/test#hash",
|
|
43
42
|
hasAttribute: () => false,
|
|
44
43
|
}),
|
|
45
44
|
})).toBe(false);
|
|
@@ -16,6 +16,7 @@ export type RwContext = {
|
|
|
16
16
|
layouts?: React.FC<LayoutProps<any>>[];
|
|
17
17
|
databases: Map<string, Kysely<any>>;
|
|
18
18
|
scriptsToBeLoaded: Set<string>;
|
|
19
|
+
pageRouteResolved: PromiseWithResolvers<void> | undefined;
|
|
19
20
|
};
|
|
20
21
|
export type RouteMiddleware<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response> | void | Promise<void> | Promise<Response | void>;
|
|
21
22
|
type RouteFunction<T extends RequestInfo = RequestInfo> = (requestInfo: T) => Response | Promise<Response>;
|
|
@@ -42,6 +43,7 @@ export declare function defineRoutes<T extends RequestInfo = RequestInfo>(routes
|
|
|
42
43
|
export declare function route<T extends RequestInfo = RequestInfo>(path: string, handler: RouteHandler<T>): RouteDefinition<T>;
|
|
43
44
|
export declare function index<T extends RequestInfo = RequestInfo>(handler: RouteHandler<T>): RouteDefinition<T>;
|
|
44
45
|
export declare function prefix<T extends RequestInfo = RequestInfo>(prefixPath: string, routes: Route<T>[]): Route<T>[];
|
|
46
|
+
export declare const wrapHandlerToThrowResponses: <T extends RequestInfo = RequestInfo>(handler: RouteFunction<T> | RouteComponent<T>) => RouteHandler<T>;
|
|
45
47
|
export declare function layout<T extends RequestInfo = RequestInfo>(LayoutComponent: React.FC<LayoutProps<T>>, routes: Route<T>[]): Route<T>[];
|
|
46
48
|
export declare function render<T extends RequestInfo = RequestInfo>(Document: React.FC<DocumentProps<T>>, routes: Route<T>[],
|
|
47
49
|
/**
|
|
@@ -53,5 +55,5 @@ options?: {
|
|
|
53
55
|
rscPayload?: boolean;
|
|
54
56
|
ssr?: boolean;
|
|
55
57
|
}): Route<T>[];
|
|
56
|
-
export declare const isClientReference: (
|
|
58
|
+
export declare const isClientReference: (value: any) => boolean;
|
|
57
59
|
export {};
|
|
@@ -97,7 +97,13 @@ export function defineRoutes(routes) {
|
|
|
97
97
|
for (const h of handlers) {
|
|
98
98
|
if (isRouteComponent(h)) {
|
|
99
99
|
const requestInfo = getRequestInfo();
|
|
100
|
-
const WrappedComponent = wrapWithLayouts(h, layouts || [], requestInfo);
|
|
100
|
+
const WrappedComponent = wrapWithLayouts(wrapHandlerToThrowResponses(h), layouts || [], requestInfo);
|
|
101
|
+
if (!isClientReference(WrappedComponent)) {
|
|
102
|
+
// context(justinvdm, 31 Jul 2025): We now know we're dealing with a page route,
|
|
103
|
+
// so we create a deferred so that we can signal when we're done determining whether
|
|
104
|
+
// we're returning a response or a react element
|
|
105
|
+
requestInfo.rw.pageRouteResolved = Promise.withResolvers();
|
|
106
|
+
}
|
|
101
107
|
return await renderPage(requestInfo, WrappedComponent, onError);
|
|
102
108
|
}
|
|
103
109
|
else {
|
|
@@ -164,6 +170,28 @@ function wrapWithLayouts(Component, layouts = [], requestInfo) {
|
|
|
164
170
|
return Wrapped;
|
|
165
171
|
}, Component);
|
|
166
172
|
}
|
|
173
|
+
// context(justinvdm, 31 Jul 2025): We need to wrap the handler's that might
|
|
174
|
+
// return react elements, so that it throws the response to bubble it up and
|
|
175
|
+
// break out of react rendering context This way, we're able to return a
|
|
176
|
+
// response from the handler while still staying within react rendering context
|
|
177
|
+
export const wrapHandlerToThrowResponses = (handler) => {
|
|
178
|
+
if (isClientReference(handler) ||
|
|
179
|
+
!isRouteComponent(handler) ||
|
|
180
|
+
Object.prototype.hasOwnProperty.call(handler, "__rwsdk_route_component")) {
|
|
181
|
+
return handler;
|
|
182
|
+
}
|
|
183
|
+
const ComponentWrappedToThrowResponses = async (requestInfo) => {
|
|
184
|
+
const result = await handler(requestInfo);
|
|
185
|
+
if (result instanceof Response) {
|
|
186
|
+
requestInfo.rw.pageRouteResolved?.reject(result);
|
|
187
|
+
throw result;
|
|
188
|
+
}
|
|
189
|
+
requestInfo.rw.pageRouteResolved?.resolve();
|
|
190
|
+
return result;
|
|
191
|
+
};
|
|
192
|
+
ComponentWrappedToThrowResponses.__rwsdk_route_component = true;
|
|
193
|
+
return ComponentWrappedToThrowResponses;
|
|
194
|
+
};
|
|
167
195
|
export function layout(LayoutComponent, routes) {
|
|
168
196
|
// Attach layouts directly to route definitions
|
|
169
197
|
return routes.map((route) => {
|
|
@@ -202,9 +230,10 @@ options = {}) {
|
|
|
202
230
|
return [documentMiddleware, ...routes];
|
|
203
231
|
}
|
|
204
232
|
function isRouteComponent(handler) {
|
|
205
|
-
return ((
|
|
233
|
+
return (Object.prototype.hasOwnProperty.call(handler, "__rwsdk_route_component") ||
|
|
234
|
+
(isValidElementType(handler) && handler.toString().includes("jsx")) ||
|
|
206
235
|
isClientReference(handler));
|
|
207
236
|
}
|
|
208
|
-
export const isClientReference = (
|
|
209
|
-
return Object.prototype.hasOwnProperty.call(
|
|
237
|
+
export const isClientReference = (value) => {
|
|
238
|
+
return Object.prototype.hasOwnProperty.call(value, "$$isClientReference");
|
|
210
239
|
};
|
|
@@ -5,7 +5,9 @@ export interface RequestInfo<Params = any, AppContext = DefaultAppContext> {
|
|
|
5
5
|
request: Request;
|
|
6
6
|
params: Params;
|
|
7
7
|
ctx: AppContext;
|
|
8
|
+
/** @deprecated: Use `response.headers` instead */
|
|
8
9
|
headers: Headers;
|
|
9
10
|
rw: RwContext;
|
|
10
11
|
cf: ExecutionContext;
|
|
12
|
+
response: ResponseInit;
|
|
11
13
|
}
|
|
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "async_hooks";
|
|
|
2
2
|
const requestInfoDeferred = Promise.withResolvers();
|
|
3
3
|
const requestInfoStore = new AsyncLocalStorage();
|
|
4
4
|
const requestInfoBase = {};
|
|
5
|
-
const REQUEST_INFO_KEYS = ["request", "params", "ctx", "headers", "rw", "cf"];
|
|
5
|
+
const REQUEST_INFO_KEYS = ["request", "params", "ctx", "headers", "rw", "cf", "response"];
|
|
6
6
|
REQUEST_INFO_KEYS.forEach((key) => {
|
|
7
7
|
Object.defineProperty(requestInfoBase, key, {
|
|
8
8
|
enumerable: true,
|
package/dist/runtime/worker.js
CHANGED
|
@@ -42,6 +42,11 @@ export const defineApp = (routes) => {
|
|
|
42
42
|
ssr: true,
|
|
43
43
|
databases: new Map(),
|
|
44
44
|
scriptsToBeLoaded: new Set(),
|
|
45
|
+
pageRouteResolved: undefined,
|
|
46
|
+
};
|
|
47
|
+
const userResponseInit = {
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: new Headers(),
|
|
45
50
|
};
|
|
46
51
|
const outerRequestInfo = {
|
|
47
52
|
request,
|
|
@@ -50,6 +55,7 @@ export const defineApp = (routes) => {
|
|
|
50
55
|
params: {},
|
|
51
56
|
ctx: {},
|
|
52
57
|
rw,
|
|
58
|
+
response: userResponseInit,
|
|
53
59
|
};
|
|
54
60
|
const createPageElement = (requestInfo, Page) => {
|
|
55
61
|
let pageElement;
|
|
@@ -59,16 +65,7 @@ export const defineApp = (routes) => {
|
|
|
59
65
|
pageElement = _jsx(Page, { ctx: ctx, params: params });
|
|
60
66
|
}
|
|
61
67
|
else {
|
|
62
|
-
|
|
63
|
-
// This way, we're able to return a response from the page component while still staying within react rendering context
|
|
64
|
-
const PageWrapper = async () => {
|
|
65
|
-
const result = await Page(requestInfo);
|
|
66
|
-
if (result instanceof Response) {
|
|
67
|
-
throw result;
|
|
68
|
-
}
|
|
69
|
-
return result;
|
|
70
|
-
};
|
|
71
|
-
pageElement = _jsx(PageWrapper, {});
|
|
68
|
+
pageElement = _jsx(Page, { ...requestInfo });
|
|
72
69
|
}
|
|
73
70
|
if (isSmokeTest) {
|
|
74
71
|
pageElement = _jsx(SmokeTestWrapper, { children: pageElement });
|
|
@@ -97,10 +94,12 @@ export const defineApp = (routes) => {
|
|
|
97
94
|
onError,
|
|
98
95
|
});
|
|
99
96
|
if (isRSCRequest) {
|
|
97
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
98
|
+
responseHeaders.set("content-type", "text/x-component; charset=utf-8");
|
|
100
99
|
return new Response(rscPayloadStream, {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
status: userResponseInit.status,
|
|
101
|
+
statusText: userResponseInit.statusText,
|
|
102
|
+
headers: responseHeaders,
|
|
104
103
|
});
|
|
105
104
|
}
|
|
106
105
|
let injectRSCPayloadStream;
|
|
@@ -120,10 +119,12 @@ export const defineApp = (routes) => {
|
|
|
120
119
|
if (injectRSCPayloadStream) {
|
|
121
120
|
html = html.pipeThrough(injectRSCPayloadStream);
|
|
122
121
|
}
|
|
122
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
123
|
+
responseHeaders.set("content-type", "text/html; charset=utf-8");
|
|
123
124
|
return new Response(html, {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
125
|
+
status: userResponseInit.status,
|
|
126
|
+
statusText: userResponseInit.statusText,
|
|
127
|
+
headers: responseHeaders,
|
|
127
128
|
});
|
|
128
129
|
};
|
|
129
130
|
const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => {
|
|
@@ -143,11 +144,22 @@ export const defineApp = (routes) => {
|
|
|
143
144
|
// context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
|
|
144
145
|
// we need to return a mutable response object.
|
|
145
146
|
const mutableResponse = new Response(response.body, response);
|
|
147
|
+
// Merge user headers from the legacy headers object
|
|
146
148
|
for (const [key, value] of userHeaders.entries()) {
|
|
147
149
|
if (!response.headers.has(key)) {
|
|
148
150
|
mutableResponse.headers.set(key, value);
|
|
149
151
|
}
|
|
150
152
|
}
|
|
153
|
+
// Merge headers from user response init (these take precedence)
|
|
154
|
+
if (userResponseInit.headers) {
|
|
155
|
+
const userResponseHeaders = new Headers(userResponseInit.headers);
|
|
156
|
+
for (const [key, value] of userResponseHeaders.entries()) {
|
|
157
|
+
if (!response.headers.has(key)) {
|
|
158
|
+
mutableResponse.headers.set(key, value);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await rw.pageRouteResolved?.promise;
|
|
151
163
|
return mutableResponse;
|
|
152
164
|
}
|
|
153
165
|
catch (e) {
|