rwsdk 1.2.10 → 1.2.11

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.
@@ -34,6 +34,13 @@ export function validateClickEvent(event, target) {
34
34
  }
35
35
  let IS_CLIENT_NAVIGATION = false;
36
36
  let scrollRestoration = null;
37
+ let currentPathKey = null;
38
+ function getLocationPathKey() {
39
+ return `${window.location.pathname ?? ""}${window.location.search ?? ""}`;
40
+ }
41
+ function getUrlPathKey(url) {
42
+ return `${url.pathname ?? ""}${url.search ?? ""}` || getLocationPathKey();
43
+ }
37
44
  export async function navigate(href, options = { history: "push" }) {
38
45
  if (!IS_CLIENT_NAVIGATION) {
39
46
  window.location.href = href;
@@ -52,6 +59,7 @@ export async function navigate(href, options = { history: "push" }) {
52
59
  else {
53
60
  scrollRestoration?.replaceEntry(href, url, nextScrollPosition);
54
61
  }
62
+ currentPathKey = getUrlPathKey(url);
55
63
  if (scrollToTop) {
56
64
  scrollRestoration?.setPendingScroll({
57
65
  ...nextScrollPosition,
@@ -109,6 +117,7 @@ export function initClientNavigation(opts = {}) {
109
117
  IS_CLIENT_NAVIGATION = true;
110
118
  scrollRestoration = createScrollRestoration();
111
119
  scrollRestoration.initialize();
120
+ currentPathKey = getLocationPathKey();
112
121
  document.addEventListener("click", async function handleClickEvent(event) {
113
122
  if (!validateClickEvent(event, event.target)) {
114
123
  return;
@@ -120,6 +129,12 @@ export function initClientNavigation(opts = {}) {
120
129
  await navigate(href, { history: "push", onNavigate: opts.onNavigate });
121
130
  }, true);
122
131
  window.addEventListener("popstate", async function handlePopState() {
132
+ const nextPathKey = getLocationPathKey();
133
+ const isHashOnlyChange = nextPathKey === currentPathKey;
134
+ currentPathKey = nextPathKey;
135
+ if (isHashOnlyChange) {
136
+ return;
137
+ }
123
138
  scrollRestoration?.restorePopStateScroll();
124
139
  await opts.onNavigate?.();
125
140
  await globalThis.__rsc_callServer(null, null, "navigation");
@@ -103,7 +103,7 @@ describe("onNavigate callback (issue #1123 regression)", () => {
103
103
  }),
104
104
  });
105
105
  vi.stubGlobal("window", {
106
- location: { href: "http://localhost/" },
106
+ location: { href: "http://localhost/", pathname: "/", search: "" },
107
107
  addEventListener: vi.fn((event, handler) => {
108
108
  if (event === "popstate")
109
109
  capturedPopstateHandler = handler;
@@ -167,6 +167,7 @@ describe("onNavigate callback (issue #1123 regression)", () => {
167
167
  const onNavigate = vi.fn();
168
168
  initClientNavigation({ onNavigate });
169
169
  expect(capturedPopstateHandler).not.toBeNull();
170
+ window.location.pathname = "/about";
170
171
  await capturedPopstateHandler();
171
172
  expect(onNavigate).toHaveBeenCalled();
172
173
  });
@@ -208,11 +209,13 @@ describe("initClientNavigation", () => {
208
209
  let capturedScrollHandler = null;
209
210
  let capturedPagehideHandler = null;
210
211
  let capturedVisibilityChangeHandler = null;
212
+ let capturedPopstateHandler = null;
211
213
  beforeEach(() => {
212
214
  historyState = {};
213
215
  capturedScrollHandler = null;
214
216
  capturedPagehideHandler = null;
215
217
  capturedVisibilityChangeHandler = null;
218
+ capturedPopstateHandler = null;
216
219
  vi.clearAllMocks();
217
220
  const mockHistory = {
218
221
  scrollRestoration: "auto",
@@ -236,7 +239,7 @@ describe("initClientNavigation", () => {
236
239
  }),
237
240
  });
238
241
  vi.stubGlobal("window", {
239
- location: { href: "http://localhost/" },
242
+ location: { href: "http://localhost/", pathname: "/", search: "" },
240
243
  addEventListener: vi.fn((event, handler) => {
241
244
  if (event === "scroll") {
242
245
  capturedScrollHandler = handler;
@@ -244,6 +247,9 @@ describe("initClientNavigation", () => {
244
247
  if (event === "pagehide") {
245
248
  capturedPagehideHandler = handler;
246
249
  }
250
+ if (event === "popstate") {
251
+ capturedPopstateHandler = handler;
252
+ }
247
253
  }),
248
254
  history: mockHistory,
249
255
  fetch: vi.fn(),
@@ -279,6 +285,22 @@ describe("initClientNavigation", () => {
279
285
  initClientNavigation();
280
286
  expect(history.scrollRestoration).toBe("manual");
281
287
  });
288
+ it("ignores hash-only popstate events so anchor links keep their native scroll", async () => {
289
+ const onNavigate = vi.fn();
290
+ globalThis.__rsc_callServer = vi.fn().mockResolvedValue(undefined);
291
+ const { onHydrated } = initClientNavigation({ onNavigate });
292
+ expect(capturedPopstateHandler).not.toBeNull();
293
+ window.location.hash =
294
+ "#heading";
295
+ window.location.href =
296
+ "http://localhost/#heading";
297
+ window.scrollY = 500;
298
+ await capturedPopstateHandler();
299
+ onHydrated();
300
+ expect(onNavigate).not.toHaveBeenCalled();
301
+ expect(globalThis.__rsc_callServer).not.toHaveBeenCalled();
302
+ expect(window.scrollTo).not.toHaveBeenCalled();
303
+ });
282
304
  it("does not write to history state on scroll", () => {
283
305
  initClientNavigation();
284
306
  expect(capturedScrollHandler).not.toBeNull();
@@ -94,10 +94,8 @@ export const transformServerFunctions = (code, normalizedId, environment, server
94
94
  process.env.VERBOSE &&
95
95
  log(`Transforming for ${environment} environment: normalizedId=%s`, normalizedId);
96
96
  const exportInfo = findExportInfo(code, normalizedId);
97
- const allExports = new Set([
98
- ...exportInfo.localFunctions,
99
- ...exportInfo.reExports.map((r) => r.localName),
100
- ]);
97
+ const reExportNames = new Set(exportInfo.reExports.map((r) => r.localName));
98
+ const allExports = new Set(Array.from(exportInfo.localFunctions).filter((name) => !reExportNames.has(name)));
101
99
  // Check for default function exports that should also be named exports
102
100
  const defaultFunctionName = findDefaultFunctionName(code, normalizedId);
103
101
  if (defaultFunctionName) {
@@ -105,11 +103,26 @@ export const transformServerFunctions = (code, normalizedId, environment, server
105
103
  }
106
104
  // Generate completely new code for SSR
107
105
  const s = new MagicString("");
108
- if (environment === "ssr") {
109
- s.append('import { createServerReference } from "rwsdk/__ssr";\n\n');
106
+ const hasDefExport = hasDefaultExport(code, normalizedId);
107
+ if (allExports.size > 0 || hasDefExport) {
108
+ if (environment === "ssr") {
109
+ s.append('import { createServerReference } from "rwsdk/__ssr";\n\n');
110
+ }
111
+ else {
112
+ s.append('import { createServerReference } from "rwsdk/client";\n\n');
113
+ }
110
114
  }
111
- else {
112
- s.append('import { createServerReference } from "rwsdk/client";\n\n');
115
+ for (const reExport of exportInfo.reExports) {
116
+ const reExportStatement = reExport.originalName === "default"
117
+ ? `export { default as ${reExport.localName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`
118
+ : reExport.originalName === reExport.localName
119
+ ? `export { ${reExport.originalName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`
120
+ : `export { ${reExport.originalName} as ${reExport.localName} } from ${JSON.stringify(reExport.moduleSpecifier)};\n`;
121
+ s.append(reExportStatement);
122
+ log(`Preserved ${environment} re-export for function: %s (original: %s) from %s in normalizedId=%s`, reExport.localName, reExport.originalName, reExport.moduleSpecifier, normalizedId);
123
+ }
124
+ if (exportInfo.reExports.length > 0 && allExports.size > 0) {
125
+ s.append("\n");
113
126
  }
114
127
  const ext = path.extname(normalizedId).toLowerCase();
115
128
  const lang = ext === ".tsx" || ext === ".jsx" ? Lang.Tsx : SgLang.TypeScript;
@@ -151,7 +164,7 @@ export const transformServerFunctions = (code, normalizedId, environment, server
151
164
  }
152
165
  }
153
166
  // Check for default export in the actual module (not re-exports)
154
- if (hasDefaultExport(code, normalizedId)) {
167
+ if (hasDefExport) {
155
168
  let method;
156
169
  let source = "action";
157
170
  const patterns = [
@@ -169,6 +169,17 @@ export const getProject = serverQuery([
169
169
  SERVER_QUERY_ARRAY_POST_CODE,
170
170
  };
171
171
  describe("TRANSFORMS", () => {
172
+ it("preserves client re-exports so serverQuery metadata comes from the defining module", () => {
173
+ const barrelResult = transformServerFunctions(`
174
+ "use server";
175
+
176
+ export { getProject } from "./queries";
177
+ `, "/actions.ts", "client", new Set());
178
+ expect(barrelResult?.code).toContain(`export { getProject } from "./queries";`);
179
+ expect(barrelResult?.code).not.toContain(`createServerReference("/actions.ts", "getProject")`);
180
+ const queryResult = transformServerFunctions(SERVER_QUERY_GET_CODE, "/queries.ts", "client", new Set());
181
+ expect(queryResult?.code).toContain(`createServerReference("/queries.ts", "getProject", "GET", "query")`);
182
+ });
172
183
  for (const [key, CODE] of Object.entries(TEST_CASES)) {
173
184
  describe(key, () => {
174
185
  it(`CLIENT`, () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {