rwsdk 0.1.25 → 0.1.27

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,6 +1,9 @@
1
+ export interface ClientNavigationOptions {
2
+ onNavigate?: () => void;
3
+ scrollToTop?: boolean;
4
+ scrollBehavior?: "auto" | "smooth" | "instant";
5
+ }
1
6
  export declare function validateClickEvent(event: MouseEvent, target: HTMLElement): boolean;
2
- export declare function initClientNavigation(opts?: {
3
- onNavigate: () => void;
4
- }): {
7
+ export declare function initClientNavigation(opts?: ClientNavigationOptions): {
5
8
  handleResponse: (response: Response) => boolean;
6
9
  };
@@ -27,12 +27,70 @@ export function validateClickEvent(event, target) {
27
27
  }
28
28
  return true;
29
29
  }
30
- export function initClientNavigation(opts = {
31
- onNavigate: async function onNavigate() {
32
- // @ts-expect-error
33
- await globalThis.__rsc_callServer();
34
- },
35
- }) {
30
+ export function initClientNavigation(opts = {}) {
31
+ // Merge user options with defaults
32
+ const options = {
33
+ onNavigate: async function onNavigate() {
34
+ // @ts-expect-error
35
+ await globalThis.__rsc_callServer();
36
+ },
37
+ scrollToTop: true,
38
+ scrollBehavior: 'instant',
39
+ ...opts,
40
+ };
41
+ // Prevent browser's automatic scroll restoration for popstate
42
+ if ('scrollRestoration' in history) {
43
+ history.scrollRestoration = 'manual';
44
+ }
45
+ // Set up scroll behavior management
46
+ let popStateWasCalled = false;
47
+ let savedScrollPosition = null;
48
+ const observer = new MutationObserver(() => {
49
+ if (popStateWasCalled && savedScrollPosition) {
50
+ // Restore scroll position for popstate navigation (always instant)
51
+ window.scrollTo({
52
+ top: savedScrollPosition.y,
53
+ left: savedScrollPosition.x,
54
+ behavior: 'instant',
55
+ });
56
+ savedScrollPosition = null;
57
+ }
58
+ else if (options.scrollToTop && !popStateWasCalled) {
59
+ // Scroll to top for anchor click navigation (configurable)
60
+ window.scrollTo({
61
+ top: 0,
62
+ left: 0,
63
+ behavior: options.scrollBehavior,
64
+ });
65
+ // Update the current history entry with the new scroll position (top)
66
+ // This ensures that if we navigate back and then forward again,
67
+ // we return to the top position, not some previous scroll position
68
+ window.history.replaceState({
69
+ ...window.history.state,
70
+ scrollX: 0,
71
+ scrollY: 0
72
+ }, "", window.location.href);
73
+ }
74
+ popStateWasCalled = false;
75
+ });
76
+ const handleScrollPopState = (event) => {
77
+ popStateWasCalled = true;
78
+ // Save the scroll position that the browser would have restored to
79
+ const state = event.state;
80
+ if (state && typeof state === 'object' && 'scrollX' in state && 'scrollY' in state) {
81
+ savedScrollPosition = { x: state.scrollX, y: state.scrollY };
82
+ }
83
+ else {
84
+ // Fallback: try to get scroll position from browser's session history
85
+ // This is a best effort since we can't directly access the browser's stored position
86
+ savedScrollPosition = { x: window.scrollX, y: window.scrollY };
87
+ }
88
+ };
89
+ const main = document.querySelector("main") || document.body;
90
+ if (main) {
91
+ window.addEventListener("popstate", handleScrollPopState);
92
+ observer.observe(main, { childList: true, subtree: true });
93
+ }
36
94
  // Intercept all anchor tag clicks
37
95
  document.addEventListener("click", async function handleClickEvent(event) {
38
96
  // Prevent default navigation
@@ -43,12 +101,18 @@ export function initClientNavigation(opts = {
43
101
  const el = event.target;
44
102
  const a = el.closest("a");
45
103
  const href = a?.getAttribute("href");
104
+ // Save current scroll position before navigating
105
+ window.history.replaceState({
106
+ path: window.location.pathname,
107
+ scrollX: window.scrollX,
108
+ scrollY: window.scrollY
109
+ }, "", window.location.href);
46
110
  window.history.pushState({ path: href }, "", window.location.origin + href);
47
- await opts.onNavigate();
111
+ await options.onNavigate();
48
112
  }, true);
49
113
  // Handle browser back/forward buttons
50
114
  window.addEventListener("popstate", async function handlePopState() {
51
- await opts.onNavigate();
115
+ await options.onNavigate();
52
116
  });
53
117
  // Return a handleResponse function for use with initClient
54
118
  return {
@@ -1 +1,6 @@
1
1
  import "./shared";
2
+ export interface ClientNavigationOptions {
3
+ onNavigate?: () => void;
4
+ scrollToTop?: boolean;
5
+ scrollBehavior?: "auto" | "smooth" | "instant";
6
+ }
@@ -15,9 +15,9 @@ const promptForDeployment = async () => {
15
15
  });
16
16
  return new Promise((resolve) => {
17
17
  // Handle Ctrl+C (SIGINT)
18
- rl.on('SIGINT', () => {
18
+ rl.on("SIGINT", () => {
19
19
  rl.close();
20
- console.log('\nDeployment cancelled.');
20
+ console.log("\nDeployment cancelled.");
21
21
  process.exit(1);
22
22
  });
23
23
  rl.question("Do you want to proceed with deployment? (y/N): ", (answer) => {
@@ -9,6 +9,7 @@ import { isJsFile } from "./isJsFile.mjs";
9
9
  import { invalidateModule } from "./invalidateModule.mjs";
10
10
  import { getShortName } from "../lib/getShortName.mjs";
11
11
  const log = debug("rwsdk:vite:hmr-plugin");
12
+ let hasErrored = false;
12
13
  const hasDirective = async (filepath, directive) => {
13
14
  if (!isJsFile(filepath)) {
14
15
  return false;
@@ -54,7 +55,30 @@ const isInUseClientGraph = ({ file, clientFiles, server, }) => {
54
55
  export const miniflareHMRPlugin = (givenOptions) => [
55
56
  {
56
57
  name: "rwsdk:miniflare-hmr",
58
+ configureServer(server) {
59
+ return () => {
60
+ server.middlewares.use(function rwsdkDevServerErrorHandler(err, _req, _res, next) {
61
+ if (err) {
62
+ hasErrored = true;
63
+ }
64
+ next(err);
65
+ });
66
+ };
67
+ },
57
68
  async hotUpdate(ctx) {
69
+ if (hasErrored) {
70
+ const shortName = getShortName(ctx.file, ctx.server.config.root);
71
+ this.environment.logger.info(`${colors.cyan(`attempting to recover from error`)}: update to ${colors.dim(shortName)}`, {
72
+ clear: true,
73
+ timestamp: true,
74
+ });
75
+ hasErrored = false;
76
+ ctx.server.hot.send({
77
+ type: "full-reload",
78
+ path: "*",
79
+ });
80
+ return [];
81
+ }
58
82
  const { clientFiles, serverFiles, viteEnvironment: { name: environment }, workerEntryPathname: entry, } = givenOptions;
59
83
  if (process.env.VERBOSE) {
60
84
  log(`Hot update: (env=${this.environment.name}) ${ctx.file}\nModule graph:\n\n${dumpFullModuleGraph(ctx.server, this.environment.name)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rwsdk",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime",
5
5
  "type": "module",
6
6
  "bin": {