rwsdk 0.1.28 → 0.1.30

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
- // Prevent browser's automatic scroll restoration for popstate
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
- // Save current scroll position before navigating
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,
@@ -43,6 +43,10 @@ export const defineApp = (routes) => {
43
43
  databases: new Map(),
44
44
  pageRouteResolved: undefined,
45
45
  };
46
+ const userResponseInit = {
47
+ status: 200,
48
+ headers: new Headers(),
49
+ };
46
50
  const outerRequestInfo = {
47
51
  request,
48
52
  headers: userHeaders,
@@ -50,6 +54,7 @@ export const defineApp = (routes) => {
50
54
  params: {},
51
55
  ctx: {},
52
56
  rw,
57
+ response: userResponseInit,
53
58
  };
54
59
  const createPageElement = (requestInfo, Page) => {
55
60
  let pageElement;
@@ -88,10 +93,12 @@ export const defineApp = (routes) => {
88
93
  onError,
89
94
  });
90
95
  if (isRSCRequest) {
96
+ const responseHeaders = new Headers(userResponseInit.headers);
97
+ responseHeaders.set("content-type", "text/x-component; charset=utf-8");
91
98
  return new Response(rscPayloadStream, {
92
- headers: {
93
- "content-type": "text/x-component; charset=utf-8",
94
- },
99
+ status: userResponseInit.status,
100
+ statusText: userResponseInit.statusText,
101
+ headers: responseHeaders,
95
102
  });
96
103
  }
97
104
  let injectRSCPayloadStream;
@@ -111,10 +118,12 @@ export const defineApp = (routes) => {
111
118
  if (injectRSCPayloadStream) {
112
119
  html = html.pipeThrough(injectRSCPayloadStream);
113
120
  }
121
+ const responseHeaders = new Headers(userResponseInit.headers);
122
+ responseHeaders.set("content-type", "text/html; charset=utf-8");
114
123
  return new Response(html, {
115
- headers: {
116
- "content-type": "text/html; charset=utf-8",
117
- },
124
+ status: userResponseInit.status,
125
+ statusText: userResponseInit.statusText,
126
+ headers: responseHeaders,
118
127
  });
119
128
  };
120
129
  const response = await runWithRequestInfo(outerRequestInfo, async () => new Promise(async (resolve, reject) => {
@@ -134,11 +143,21 @@ export const defineApp = (routes) => {
134
143
  // context(justinvdm, 18 Mar 2025): In some cases, such as a .fetch() call to a durable object instance, or Response.redirect(),
135
144
  // we need to return a mutable response object.
136
145
  const mutableResponse = new Response(response.body, response);
146
+ // Merge user headers from the legacy headers object
137
147
  for (const [key, value] of userHeaders.entries()) {
138
148
  if (!response.headers.has(key)) {
139
149
  mutableResponse.headers.set(key, value);
140
150
  }
141
151
  }
152
+ // Merge headers from user response init (these take precedence)
153
+ if (userResponseInit.headers) {
154
+ const userResponseHeaders = new Headers(userResponseInit.headers);
155
+ for (const [key, value] of userResponseHeaders.entries()) {
156
+ if (!response.headers.has(key)) {
157
+ mutableResponse.headers.set(key, value);
158
+ }
159
+ }
160
+ }
142
161
  await rw.pageRouteResolved?.promise;
143
162
  return mutableResponse;
144
163
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.1.28",
3
+ "version": "0.1.30",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {