rwsdk 0.2.0-alpha.1 → 0.2.0-alpha.3
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.
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
function saveScrollPosition(x, y) {
|
|
2
|
+
window.history.replaceState({
|
|
3
|
+
...window.history.state,
|
|
4
|
+
scrollX: x,
|
|
5
|
+
scrollY: y,
|
|
6
|
+
}, "", window.location.href);
|
|
7
|
+
}
|
|
1
8
|
export function validateClickEvent(event, target) {
|
|
2
9
|
// should this only work for left click?
|
|
3
10
|
if (event.button !== 0) {
|
|
@@ -31,7 +38,6 @@ export function validateClickEvent(event, target) {
|
|
|
31
38
|
return true;
|
|
32
39
|
}
|
|
33
40
|
export function initClientNavigation(opts = {}) {
|
|
34
|
-
// Merge user options with defaults
|
|
35
41
|
const options = {
|
|
36
42
|
onNavigate: async function onNavigate() {
|
|
37
43
|
// @ts-expect-error
|
|
@@ -41,63 +47,7 @@ export function initClientNavigation(opts = {}) {
|
|
|
41
47
|
scrollBehavior: "instant",
|
|
42
48
|
...opts,
|
|
43
49
|
};
|
|
44
|
-
|
|
45
|
-
if ("scrollRestoration" in history) {
|
|
46
|
-
history.scrollRestoration = "manual";
|
|
47
|
-
}
|
|
48
|
-
// Set up scroll behavior management
|
|
49
|
-
let popStateWasCalled = false;
|
|
50
|
-
let savedScrollPosition = null;
|
|
51
|
-
const observer = new MutationObserver(() => {
|
|
52
|
-
if (popStateWasCalled && savedScrollPosition) {
|
|
53
|
-
// Restore scroll position for popstate navigation (always instant)
|
|
54
|
-
window.scrollTo({
|
|
55
|
-
top: savedScrollPosition.y,
|
|
56
|
-
left: savedScrollPosition.x,
|
|
57
|
-
behavior: "instant",
|
|
58
|
-
});
|
|
59
|
-
savedScrollPosition = null;
|
|
60
|
-
}
|
|
61
|
-
else if (options.scrollToTop && !popStateWasCalled) {
|
|
62
|
-
// Scroll to top for anchor click navigation (configurable)
|
|
63
|
-
window.scrollTo({
|
|
64
|
-
top: 0,
|
|
65
|
-
left: 0,
|
|
66
|
-
behavior: options.scrollBehavior,
|
|
67
|
-
});
|
|
68
|
-
// Update the current history entry with the new scroll position (top)
|
|
69
|
-
// This ensures that if we navigate back and then forward again,
|
|
70
|
-
// we return to the top position, not some previous scroll position
|
|
71
|
-
window.history.replaceState({
|
|
72
|
-
...window.history.state,
|
|
73
|
-
scrollX: 0,
|
|
74
|
-
scrollY: 0,
|
|
75
|
-
}, "", window.location.href);
|
|
76
|
-
}
|
|
77
|
-
popStateWasCalled = false;
|
|
78
|
-
});
|
|
79
|
-
const handleScrollPopState = (event) => {
|
|
80
|
-
popStateWasCalled = true;
|
|
81
|
-
// Save the scroll position that the browser would have restored to
|
|
82
|
-
const state = event.state;
|
|
83
|
-
if (state &&
|
|
84
|
-
typeof state === "object" &&
|
|
85
|
-
"scrollX" in state &&
|
|
86
|
-
"scrollY" in state) {
|
|
87
|
-
savedScrollPosition = { x: state.scrollX, y: state.scrollY };
|
|
88
|
-
}
|
|
89
|
-
else {
|
|
90
|
-
// Fallback: try to get scroll position from browser's session history
|
|
91
|
-
// This is a best effort since we can't directly access the browser's stored position
|
|
92
|
-
savedScrollPosition = { x: window.scrollX, y: window.scrollY };
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
const main = document.querySelector("main") || document.body;
|
|
96
|
-
if (main) {
|
|
97
|
-
window.addEventListener("popstate", handleScrollPopState);
|
|
98
|
-
observer.observe(main, { childList: true, subtree: true });
|
|
99
|
-
}
|
|
100
|
-
// Intercept all anchor tag clicks
|
|
50
|
+
history.scrollRestoration = "auto";
|
|
101
51
|
document.addEventListener("click", async function handleClickEvent(event) {
|
|
102
52
|
// Prevent default navigation
|
|
103
53
|
if (!validateClickEvent(event, event.target)) {
|
|
@@ -107,17 +57,21 @@ export function initClientNavigation(opts = {}) {
|
|
|
107
57
|
const el = event.target;
|
|
108
58
|
const a = el.closest("a");
|
|
109
59
|
const href = a?.getAttribute("href");
|
|
110
|
-
|
|
111
|
-
window.history.replaceState({
|
|
112
|
-
path: window.location.pathname,
|
|
113
|
-
scrollX: window.scrollX,
|
|
114
|
-
scrollY: window.scrollY,
|
|
115
|
-
}, "", window.location.href);
|
|
60
|
+
saveScrollPosition(window.scrollX, window.scrollY);
|
|
116
61
|
window.history.pushState({ path: href }, "", window.location.origin + href);
|
|
117
62
|
await options.onNavigate();
|
|
63
|
+
if (options.scrollToTop && history.scrollRestoration === "auto") {
|
|
64
|
+
window.scrollTo({
|
|
65
|
+
top: 0,
|
|
66
|
+
left: 0,
|
|
67
|
+
behavior: options.scrollBehavior,
|
|
68
|
+
});
|
|
69
|
+
saveScrollPosition(0, 0);
|
|
70
|
+
}
|
|
71
|
+
history.scrollRestoration = "auto";
|
|
118
72
|
}, true);
|
|
119
|
-
// Handle browser back/forward buttons
|
|
120
73
|
window.addEventListener("popstate", async function handlePopState() {
|
|
74
|
+
saveScrollPosition(window.scrollX, window.scrollY);
|
|
121
75
|
await options.onNavigate();
|
|
122
76
|
});
|
|
123
77
|
// Return a handleResponse function for use with initClient
|
|
@@ -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
|
@@ -44,6 +44,10 @@ export const defineApp = (routes) => {
|
|
|
44
44
|
scriptsToBeLoaded: new Set(),
|
|
45
45
|
pageRouteResolved: undefined,
|
|
46
46
|
};
|
|
47
|
+
const userResponseInit = {
|
|
48
|
+
status: 200,
|
|
49
|
+
headers: new Headers(),
|
|
50
|
+
};
|
|
47
51
|
const outerRequestInfo = {
|
|
48
52
|
request,
|
|
49
53
|
headers: userHeaders,
|
|
@@ -51,6 +55,7 @@ export const defineApp = (routes) => {
|
|
|
51
55
|
params: {},
|
|
52
56
|
ctx: {},
|
|
53
57
|
rw,
|
|
58
|
+
response: userResponseInit,
|
|
54
59
|
};
|
|
55
60
|
const createPageElement = (requestInfo, Page) => {
|
|
56
61
|
let pageElement;
|
|
@@ -89,10 +94,12 @@ export const defineApp = (routes) => {
|
|
|
89
94
|
onError,
|
|
90
95
|
});
|
|
91
96
|
if (isRSCRequest) {
|
|
97
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
98
|
+
responseHeaders.set("content-type", "text/x-component; charset=utf-8");
|
|
92
99
|
return new Response(rscPayloadStream, {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
100
|
+
status: userResponseInit.status,
|
|
101
|
+
statusText: userResponseInit.statusText,
|
|
102
|
+
headers: responseHeaders,
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
let injectRSCPayloadStream;
|
|
@@ -112,10 +119,12 @@ export const defineApp = (routes) => {
|
|
|
112
119
|
if (injectRSCPayloadStream) {
|
|
113
120
|
html = html.pipeThrough(injectRSCPayloadStream);
|
|
114
121
|
}
|
|
122
|
+
const responseHeaders = new Headers(userResponseInit.headers);
|
|
123
|
+
responseHeaders.set("content-type", "text/html; charset=utf-8");
|
|
115
124
|
return new Response(html, {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
status: userResponseInit.status,
|
|
126
|
+
statusText: userResponseInit.statusText,
|
|
127
|
+
headers: responseHeaders,
|
|
119
128
|
});
|
|
120
129
|
};
|
|
121
130
|
const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => {
|
|
@@ -135,11 +144,21 @@ export const defineApp = (routes) => {
|
|
|
135
144
|
// context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
|
|
136
145
|
// we need to return a mutable response object.
|
|
137
146
|
const mutableResponse = new Response(response.body, response);
|
|
147
|
+
// Merge user headers from the legacy headers object
|
|
138
148
|
for (const [key, value] of userHeaders.entries()) {
|
|
139
149
|
if (!response.headers.has(key)) {
|
|
140
150
|
mutableResponse.headers.set(key, value);
|
|
141
151
|
}
|
|
142
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
|
+
}
|
|
143
162
|
await rw.pageRouteResolved?.promise;
|
|
144
163
|
return mutableResponse;
|
|
145
164
|
}
|