@ecopages/react-router 0.2.0-alpha.9 → 0.2.1-beta.0

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/src/router.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  useEffect,
3
+ useEffectEvent,
3
4
  useState,
4
5
  useCallback,
5
6
  useMemo,
@@ -14,6 +15,7 @@ import { RouterContext } from "./context.js";
14
15
  import {
15
16
  fetchPageDocument,
16
17
  getInterceptDecision,
18
+ isSamePageHashNavigationHref,
17
19
  loadPageModuleFromDocument,
18
20
  shouldInterceptClick
19
21
  } from "./navigation.js";
@@ -21,7 +23,9 @@ import { morphHead } from "./head-morpher.js";
21
23
  import { applyViewTransitionNames } from "./view-transition-utils.js";
22
24
  import { manageScroll } from "./manage-scroll.js";
23
25
  import { saveScrollPositions, restoreScrollPositions } from "./scroll-persist.js";
24
- import { getEcoNavigationRuntime } from "@ecopages/core/router/navigation-coordinator";
26
+ import {
27
+ getEcoNavigationRuntime
28
+ } from "@ecopages/core/router/navigation-coordinator";
25
29
  import {
26
30
  getAnchorFromNavigationEvent,
27
31
  recoverPendingNavigationHref
@@ -52,6 +56,27 @@ function normalizeLayoutKey(value) {
52
56
  return trimmed.split("#")[0]?.split("?")[0]?.replace(/\/$/, "") || "layout";
53
57
  }
54
58
  }
59
+ function hashString(value) {
60
+ let hash = 2166136261;
61
+ for (let index = 0; index < value.length; index += 1) {
62
+ hash ^= value.charCodeAt(index);
63
+ hash = Math.imul(hash, 16777619);
64
+ }
65
+ return (hash >>> 0).toString(36);
66
+ }
67
+ function getLayoutSourceSignature(Layout) {
68
+ const source = Function.prototype.toString.call(Layout).replace(/\s+/g, " ").trim();
69
+ return hashString(source);
70
+ }
71
+ function getLayoutCacheKey(Layout) {
72
+ const layoutConfig = Layout.config;
73
+ const layoutMetaKey = layoutConfig?.__eco?.file || layoutConfig?.__eco?.id;
74
+ if (layoutMetaKey) {
75
+ return normalizeLayoutKey(layoutMetaKey);
76
+ }
77
+ const layoutNameKey = Layout.displayName || Layout.name || "layout";
78
+ return `${normalizeLayoutKey(layoutNameKey)}:${getLayoutSourceSignature(Layout)}`;
79
+ }
55
80
  function clearLayoutCache() {
56
81
  getLayoutCache().clear();
57
82
  }
@@ -64,7 +89,7 @@ const PageContent = () => {
64
89
  }
65
90
  return null;
66
91
  }
67
- const { Component: Page, props } = pageContext;
92
+ const { Component: Page, props, refreshPersistedLayout } = pageContext;
68
93
  const Layout = getLayoutFromPage(Page);
69
94
  const pageElement = createElement(Page, props);
70
95
  const layoutProps = props?.locals ? { locals: props.locals } : null;
@@ -73,10 +98,8 @@ const PageContent = () => {
73
98
  }
74
99
  if (persistLayouts) {
75
100
  const layoutCache = getLayoutCache();
76
- const layoutConfig = Layout.config;
77
- const layoutKeyRaw = layoutConfig?.__eco?.id || Layout.displayName || Layout.name || "layout";
78
- const layoutKey = normalizeLayoutKey(layoutKeyRaw);
79
- if (!layoutCache.has(layoutKey)) {
101
+ const layoutKey = getLayoutCacheKey(Layout);
102
+ if (!layoutCache.has(layoutKey) || refreshPersistedLayout && layoutCache.get(layoutKey) !== Layout) {
80
103
  layoutCache.set(layoutKey, Layout);
81
104
  }
82
105
  const CachedLayout = layoutCache.get(layoutKey);
@@ -92,35 +115,39 @@ function createDeferred() {
92
115
  return { promise, resolve };
93
116
  }
94
117
  function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef) {
118
+ const handleCoordinatorNavigate = useEffectEvent(async (request) => {
119
+ await navigate(request.href, {
120
+ isPopState: request.direction === "back",
121
+ pushHistory: request.direction === "forward",
122
+ skipViewTransition: request.source === "browser-router"
123
+ });
124
+ return true;
125
+ });
126
+ const handleCoordinatorReload = useEffectEvent(async (request) => {
127
+ if (activeNavigationRef.current || isNavigatingRef.current) {
128
+ return;
129
+ }
130
+ if (request?.clearCache) {
131
+ clearLayoutCache();
132
+ }
133
+ const currentUrl = window.location.pathname + window.location.search;
134
+ await navigate(currentUrl, { moduleUrlOverride: request?.moduleUrl });
135
+ });
136
+ const handleCleanupBeforeHandoff = useEffectEvent(async () => {
137
+ runtimeActiveRef.current = false;
138
+ window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
139
+ });
95
140
  useEffect(() => {
96
- if (typeof window === "undefined") return;
97
141
  const navigationRuntime = getEcoNavigationRuntime(window);
98
142
  let unregisterRuntime = null;
99
143
  const unregister = navigationRuntime.register({
100
144
  owner: "react-router",
101
- navigate: async (request) => {
102
- await navigate(request.href, {
103
- isPopState: request.direction === "back",
104
- pushHistory: request.direction === "forward",
105
- skipViewTransition: request.source === "browser-router"
106
- });
107
- return true;
108
- },
109
- reloadCurrentPage: async (request) => {
110
- if (activeNavigationRef.current || isNavigatingRef.current) {
111
- return;
112
- }
113
- if (request?.clearCache) {
114
- clearLayoutCache();
115
- }
116
- const currentUrl = window.location.pathname + window.location.search;
117
- await navigate(currentUrl);
118
- },
145
+ navigate: handleCoordinatorNavigate,
146
+ reloadCurrentPage: handleCoordinatorReload,
119
147
  cleanupBeforeHandoff: async () => {
120
- runtimeActiveRef.current = false;
121
148
  unregisterRuntime?.();
122
149
  unregisterRuntime = null;
123
- window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
150
+ await handleCleanupBeforeHandoff();
124
151
  }
125
152
  });
126
153
  unregisterRuntime = unregister;
@@ -132,11 +159,15 @@ function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef
132
159
  unregisterRuntime?.();
133
160
  unregisterRuntime = null;
134
161
  };
135
- }, [activeNavigationRef, isNavigatingRef, navigate, runtimeActiveRef]);
162
+ }, [runtimeActiveRef]);
136
163
  }
137
164
  const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
138
165
  const options = useMemo(() => ({ ...DEFAULT_OPTIONS, ...userOptions }), [userOptions]);
139
- const [currentPage, setCurrentPage] = useState({ Component: page, props: pageProps });
166
+ const [currentPage, setCurrentPage] = useState({
167
+ Component: page,
168
+ props: pageProps,
169
+ refreshPersistedLayout: false
170
+ });
140
171
  const [isNavigating, setIsNavigating] = useState(false);
141
172
  const pendingRenderRef = useRef(null);
142
173
  const activeNavigationRef = useRef(null);
@@ -145,34 +176,24 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
145
176
  const pendingPointerNavigationRef = useRef(null);
146
177
  const pendingHoverNavigationRef = useRef(null);
147
178
  const queuedNavigationHrefRef = useRef(null);
148
- const pendingScrollRestoreRef = useRef(null);
179
+ const committedPathRef = useRef(
180
+ typeof window !== "undefined" ? window.location.pathname + window.location.search : ""
181
+ );
149
182
  const previousUrlRef = useRef(typeof window !== "undefined" ? window.location.href : "");
150
183
  useEffect(() => {
151
184
  isNavigatingRef.current = isNavigating;
152
185
  }, [isNavigating]);
153
186
  useEffect(() => {
154
- setCurrentPage({ Component: page, props: pageProps });
187
+ setCurrentPage({ Component: page, props: pageProps, refreshPersistedLayout: true });
155
188
  }, [page, pageProps]);
156
189
  useEffect(() => {
190
+ committedPathRef.current = window.location.pathname + window.location.search;
157
191
  applyViewTransitionNames();
158
- }, [currentPage]);
159
- useEffect(() => {
160
192
  const pendingRender = pendingRenderRef.current;
161
193
  if (pendingRender && currentPage.Component === pendingRender.page.Component && currentPage.props === pendingRender.page.props) {
162
194
  pendingRender.resolve();
163
195
  pendingRenderRef.current = null;
164
196
  }
165
- }, [currentPage]);
166
- useEffect(() => {
167
- return () => {
168
- activeNavigationRef.current?.cancel();
169
- pendingRenderRef.current?.resolve();
170
- pendingRenderRef.current = null;
171
- queuedNavigationHrefRef.current = null;
172
- };
173
- }, []);
174
- useEffect(() => {
175
- if (typeof window === "undefined") return;
176
197
  const url = new URL(window.location.href);
177
198
  const previousUrl = new URL(previousUrlRef.current);
178
199
  if (url.href !== previousUrl.href) {
@@ -182,15 +203,23 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
182
203
  });
183
204
  previousUrlRef.current = url.href;
184
205
  }
185
- if (pendingScrollRestoreRef.current) {
186
- const { url: targetUrl, isPopState } = pendingScrollRestoreRef.current;
187
- restoreScrollPositions(targetUrl, isPopState);
188
- pendingScrollRestoreRef.current = null;
189
- }
190
206
  }, [currentPage, options.scrollBehavior, options.smoothScroll]);
207
+ useEffect(() => {
208
+ return () => {
209
+ activeNavigationRef.current?.cancel();
210
+ pendingRenderRef.current?.resolve();
211
+ pendingRenderRef.current = null;
212
+ queuedNavigationHrefRef.current = null;
213
+ };
214
+ }, []);
191
215
  const navigate = useCallback(
192
216
  async (url, navigationOptions = {}) => {
193
- const { isPopState = false, pushHistory = false, skipViewTransition = false } = navigationOptions;
217
+ const {
218
+ isPopState = false,
219
+ pushHistory = false,
220
+ skipViewTransition = false,
221
+ moduleUrlOverride
222
+ } = navigationOptions;
194
223
  const navigationRuntime = getEcoNavigationRuntime(window);
195
224
  const navigation = navigationRuntime.beginNavigationTransaction();
196
225
  activeNavigationRef.current = navigation;
@@ -203,6 +232,16 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
203
232
  props
204
233
  };
205
234
  };
235
+ const preparePendingRender = (nextPage) => {
236
+ pendingRenderRef.current?.resolve();
237
+ const renderDfd = createDeferred();
238
+ pendingRenderRef.current = {
239
+ navigationId,
240
+ page: nextPage,
241
+ resolve: renderDfd.resolve
242
+ };
243
+ return renderDfd.promise;
244
+ };
206
245
  let navigationCommitPromise = null;
207
246
  try {
208
247
  setIsNavigating(true);
@@ -212,32 +251,38 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
212
251
  window.location.href = url;
213
252
  return;
214
253
  }
215
- const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath);
254
+ const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath, {
255
+ moduleUrlOverride
256
+ });
216
257
  if (isStale()) return;
217
258
  if (result) {
218
259
  const { Component, props, doc, finalPath, moduleUrl } = result;
219
- const nextPage = { Component, props };
220
- const cleanupHead = await morphHead(doc);
260
+ const nextPage = { Component, props, refreshPersistedLayout: Boolean(moduleUrlOverride) };
261
+ const { cleanup: cleanupHead, flushRerunScripts } = await morphHead(doc);
262
+ const finalizeCommittedNavigation = () => {
263
+ committedPathRef.current = finalPath;
264
+ flushRerunScripts();
265
+ cleanupHead();
266
+ applyViewTransitionNames();
267
+ restoreScrollPositions(finalPath, isPopState);
268
+ };
269
+ const commitNextPage = () => {
270
+ commitPageData(moduleUrl, props);
271
+ setCurrentPage(nextPage);
272
+ };
221
273
  if (isStale()) {
222
274
  cleanupHead();
223
275
  return;
224
276
  }
225
277
  applyViewTransitionNames();
278
+ saveScrollPositions();
226
279
  if (pushHistory) {
227
280
  window.history.pushState(null, "", finalPath);
228
281
  } else if (finalPath !== url) {
229
282
  window.history.replaceState(null, "", finalPath);
230
283
  }
231
- saveScrollPositions();
232
- pendingScrollRestoreRef.current = { url, isPopState };
233
284
  if (!skipViewTransition && options.viewTransitions && document.startViewTransition) {
234
- pendingRenderRef.current?.resolve();
235
- const renderDfd = createDeferred();
236
- pendingRenderRef.current = {
237
- navigationId,
238
- page: nextPage,
239
- resolve: renderDfd.resolve
240
- };
285
+ const renderPromise = preparePendingRender(nextPage);
241
286
  navigationCommitPromise = new Promise((resolve) => {
242
287
  document.startViewTransition(async () => {
243
288
  try {
@@ -250,15 +295,13 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
250
295
  return;
251
296
  }
252
297
  startTransition(() => {
253
- commitPageData(moduleUrl, props);
254
- setCurrentPage(nextPage);
298
+ commitNextPage();
255
299
  });
256
- await renderDfd.promise;
300
+ await renderPromise;
257
301
  if (isStale()) {
258
302
  return;
259
303
  }
260
- cleanupHead();
261
- applyViewTransitionNames();
304
+ finalizeCommittedNavigation();
262
305
  } finally {
263
306
  resolve();
264
307
  }
@@ -266,10 +309,14 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
266
309
  });
267
310
  await navigationCommitPromise;
268
311
  } else {
269
- commitPageData(moduleUrl, props);
270
- setCurrentPage(nextPage);
271
- cleanupHead();
272
- applyViewTransitionNames();
312
+ const renderPromise = preparePendingRender(nextPage);
313
+ commitNextPage();
314
+ await renderPromise;
315
+ if (isStale()) {
316
+ cleanupHead();
317
+ return;
318
+ }
319
+ finalizeCommittedNavigation();
273
320
  }
274
321
  } else {
275
322
  if (isStale()) return;
@@ -293,11 +340,12 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
293
340
  } finally {
294
341
  const shouldReplayQueuedNavigation = activeNavigationRef.current?.id === navigationId;
295
342
  const queuedNavigationHref = shouldReplayQueuedNavigation ? queuedNavigationHrefRef.current : null;
343
+ const queuedNavigationPath = queuedNavigationHref ? new URL(queuedNavigationHref, window.location.origin).pathname + new URL(queuedNavigationHref, window.location.origin).search : null;
296
344
  navigation.complete();
297
345
  if (activeNavigationRef.current?.id === navigationId) {
298
346
  activeNavigationRef.current = null;
299
347
  }
300
- if (queuedNavigationHref && queuedNavigationHref !== window.location.pathname + window.location.search) {
348
+ if (queuedNavigationHref && queuedNavigationPath !== committedPathRef.current) {
301
349
  queuedNavigationHrefRef.current = null;
302
350
  if (runtimeActiveRef.current) {
303
351
  void navigate(queuedNavigationHref, { pushHistory: true });
@@ -317,132 +365,150 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
317
365
  },
318
366
  [options.viewTransitions]
319
367
  );
320
- useEffect(() => {
321
- const getLinkFromEvent = (event) => getAnchorFromNavigationEvent(event, options.linkSelector);
322
- const getRecoveredPointerHref = () => {
323
- const href = recoverPendingNavigationHref(
324
- pendingPointerNavigationRef.current,
325
- !!activeNavigationRef.current || isNavigatingRef.current,
326
- performance.now()
327
- );
328
- if (!href) {
329
- pendingPointerNavigationRef.current = null;
330
- }
331
- return href;
332
- };
333
- const getRecoveredHoverHref = () => {
334
- const href = recoverPendingNavigationHref(
335
- pendingHoverNavigationRef.current,
336
- !!activeNavigationRef.current || isNavigatingRef.current,
337
- performance.now()
338
- );
339
- if (!href) {
340
- pendingHoverNavigationRef.current = null;
341
- }
342
- return href;
368
+ const getLinkFromEvent = useEffectEvent(
369
+ (event) => getAnchorFromNavigationEvent(event, options.linkSelector)
370
+ );
371
+ const getRecoveredPointerHref = useEffectEvent(() => {
372
+ const href = recoverPendingNavigationHref(
373
+ pendingPointerNavigationRef.current,
374
+ !!activeNavigationRef.current || isNavigatingRef.current,
375
+ performance.now()
376
+ );
377
+ if (!href) {
378
+ pendingPointerNavigationRef.current = null;
379
+ }
380
+ return href;
381
+ });
382
+ const getRecoveredHoverHref = useEffectEvent(() => {
383
+ const href = recoverPendingNavigationHref(
384
+ pendingHoverNavigationRef.current,
385
+ !!activeNavigationRef.current || isNavigatingRef.current,
386
+ performance.now()
387
+ );
388
+ if (!href) {
389
+ pendingHoverNavigationRef.current = null;
390
+ }
391
+ return href;
392
+ });
393
+ const handleHoverIntent = useEffectEvent((event) => {
394
+ if (!runtimeActiveRef.current) {
395
+ return;
396
+ }
397
+ const link = getLinkFromEvent(event);
398
+ if (!link) {
399
+ return;
400
+ }
401
+ const decision = getInterceptDecision(event, link, options);
402
+ if (!decision.shouldIntercept) {
403
+ return;
404
+ }
405
+ pendingHoverNavigationRef.current = {
406
+ href: link.getAttribute("href"),
407
+ timestamp: performance.now()
343
408
  };
344
- const handleHoverIntent = (event) => {
345
- if (!runtimeActiveRef.current) {
346
- return;
347
- }
348
- const link = getLinkFromEvent(event);
349
- if (!link) {
350
- return;
351
- }
352
- const decision = getInterceptDecision(event, link, options);
353
- if (!decision.shouldIntercept) {
354
- return;
355
- }
356
- pendingHoverNavigationRef.current = {
357
- href: link.getAttribute("href"),
358
- timestamp: performance.now()
359
- };
409
+ queuedNavigationHrefRef.current = link.getAttribute("href");
410
+ });
411
+ const handlePointerDown = useEffectEvent((event) => {
412
+ if (!runtimeActiveRef.current) {
413
+ pendingPointerNavigationRef.current = null;
414
+ return;
415
+ }
416
+ const link = getLinkFromEvent(event);
417
+ if (!link) {
418
+ pendingPointerNavigationRef.current = null;
419
+ return;
420
+ }
421
+ const decision = getInterceptDecision(event, link, options);
422
+ pendingPointerNavigationRef.current = decision.shouldIntercept ? {
423
+ href: link.getAttribute("href"),
424
+ timestamp: performance.now()
425
+ } : null;
426
+ if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
360
427
  queuedNavigationHrefRef.current = link.getAttribute("href");
361
- };
362
- const handlePointerDown = (event) => {
363
- if (!runtimeActiveRef.current) {
364
- pendingPointerNavigationRef.current = null;
365
- return;
366
- }
367
- const link = getLinkFromEvent(event);
368
- if (!link) {
369
- pendingPointerNavigationRef.current = null;
370
- return;
371
- }
372
- const decision = getInterceptDecision(event, link, options);
373
- pendingPointerNavigationRef.current = decision.shouldIntercept ? {
374
- href: link.getAttribute("href"),
375
- timestamp: performance.now()
376
- } : null;
377
- if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
378
- queuedNavigationHrefRef.current = link.getAttribute("href");
379
- }
380
- };
381
- const handleClick = (event) => {
382
- if (!runtimeActiveRef.current) {
383
- pendingPointerNavigationRef.current = null;
384
- pendingHoverNavigationRef.current = null;
428
+ }
429
+ });
430
+ const handleClick = useEffectEvent((event) => {
431
+ if (!runtimeActiveRef.current) {
432
+ pendingPointerNavigationRef.current = null;
433
+ pendingHoverNavigationRef.current = null;
434
+ return;
435
+ }
436
+ const link = getLinkFromEvent(event);
437
+ if (!link) {
438
+ const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
439
+ pendingPointerNavigationRef.current = null;
440
+ pendingHoverNavigationRef.current = null;
441
+ if (!recoveredHref) {
385
442
  return;
386
443
  }
387
- const link = getLinkFromEvent(event);
388
- if (!link) {
389
- const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
390
- pendingPointerNavigationRef.current = null;
391
- pendingHoverNavigationRef.current = null;
392
- if (!recoveredHref) {
393
- return;
394
- }
395
- event.preventDefault();
444
+ if (isSamePageHashNavigationHref(recoveredHref)) {
396
445
  queuedNavigationHrefRef.current = null;
397
- const recoveredUrl = new URL(recoveredHref, window.location.origin);
398
- navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
399
446
  return;
400
447
  }
401
- if (!shouldInterceptClick(event, link, options)) {
402
- if (options.debug) {
403
- const decision = getInterceptDecision(event, link, options);
404
- if (!decision.shouldIntercept) {
405
- console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
406
- }
407
- }
408
- pendingPointerNavigationRef.current = null;
409
- pendingHoverNavigationRef.current = null;
410
- return;
411
- }
412
- pendingPointerNavigationRef.current = null;
413
- pendingHoverNavigationRef.current = null;
414
448
  event.preventDefault();
415
449
  queuedNavigationHrefRef.current = null;
416
- const href = link.getAttribute("href");
417
- const url = new URL(href, window.location.origin);
450
+ const recoveredUrl = new URL(recoveredHref, window.location.href);
451
+ navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
452
+ return;
453
+ }
454
+ if (!shouldInterceptClick(event, link, options)) {
418
455
  if (options.debug) {
419
- console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
456
+ const decision = getInterceptDecision(event, link, options);
457
+ if (!decision.shouldIntercept) {
458
+ console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
459
+ }
420
460
  }
421
- navigate(url.pathname + url.search, { pushHistory: true });
461
+ pendingPointerNavigationRef.current = null;
462
+ pendingHoverNavigationRef.current = null;
463
+ return;
464
+ }
465
+ pendingPointerNavigationRef.current = null;
466
+ pendingHoverNavigationRef.current = null;
467
+ event.preventDefault();
468
+ queuedNavigationHrefRef.current = null;
469
+ const href = link.getAttribute("href");
470
+ const url = new URL(href, window.location.origin);
471
+ if (options.debug) {
472
+ console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
473
+ }
474
+ navigate(url.pathname + url.search, { pushHistory: true });
475
+ });
476
+ const handlePopState = useEffectEvent(() => {
477
+ if (!runtimeActiveRef.current) {
478
+ return;
479
+ }
480
+ navigate(window.location.pathname + window.location.search, { isPopState: true });
481
+ });
482
+ useEffect(() => {
483
+ const onHoverIntent = (event) => {
484
+ handleHoverIntent(event);
422
485
  };
423
- const handlePopState = () => {
424
- if (!runtimeActiveRef.current) {
425
- return;
426
- }
427
- navigate(window.location.pathname + window.location.search, { isPopState: true });
486
+ const onPointerDown = (event) => {
487
+ handlePointerDown(event);
428
488
  };
429
- document.addEventListener("mouseover", handleHoverIntent, true);
430
- document.addEventListener("pointerover", handleHoverIntent, true);
431
- document.addEventListener("mousemove", handleHoverIntent, true);
432
- document.addEventListener("pointermove", handleHoverIntent, true);
433
- document.addEventListener("pointerdown", handlePointerDown, true);
434
- document.addEventListener("click", handleClick, true);
435
- window.addEventListener("popstate", handlePopState);
489
+ const onClick = (event) => {
490
+ handleClick(event);
491
+ };
492
+ const onPopState = () => {
493
+ handlePopState();
494
+ };
495
+ document.addEventListener("mouseover", onHoverIntent, true);
496
+ document.addEventListener("pointerover", onHoverIntent, true);
497
+ document.addEventListener("mousemove", onHoverIntent, true);
498
+ document.addEventListener("pointermove", onHoverIntent, true);
499
+ document.addEventListener("pointerdown", onPointerDown, true);
500
+ document.addEventListener("click", onClick, true);
501
+ window.addEventListener("popstate", onPopState);
436
502
  return () => {
437
- document.removeEventListener("mouseover", handleHoverIntent, true);
438
- document.removeEventListener("pointerover", handleHoverIntent, true);
439
- document.removeEventListener("mousemove", handleHoverIntent, true);
440
- document.removeEventListener("pointermove", handleHoverIntent, true);
441
- document.removeEventListener("pointerdown", handlePointerDown, true);
442
- document.removeEventListener("click", handleClick, true);
443
- window.removeEventListener("popstate", handlePopState);
503
+ document.removeEventListener("mouseover", onHoverIntent, true);
504
+ document.removeEventListener("pointerover", onHoverIntent, true);
505
+ document.removeEventListener("mousemove", onHoverIntent, true);
506
+ document.removeEventListener("pointermove", onHoverIntent, true);
507
+ document.removeEventListener("pointerdown", onPointerDown, true);
508
+ document.removeEventListener("click", onClick, true);
509
+ window.removeEventListener("popstate", onPopState);
444
510
  };
445
- }, [navigate, options, runtimeActiveRef]);
511
+ }, []);
446
512
  useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef);
447
513
  return createElement(
448
514
  RouterContext.Provider,
@@ -28,19 +28,27 @@ function restoreScrollPositions(targetUrl, isPopState) {
28
28
  currentScrollSnapshot = null;
29
29
  return;
30
30
  }
31
- requestAnimationFrame(() => {
31
+ const restore = (remainingAttempts) => {
32
32
  requestAnimationFrame(() => {
33
+ const restoredKeys = /* @__PURE__ */ new Set();
33
34
  document.querySelectorAll(PERSIST_SELECTOR).forEach((el) => {
34
35
  const key = getElementKey(el);
35
- if (key && positions.has(key)) {
36
- const pos = positions.get(key);
37
- el.scrollTop = pos.top;
38
- el.scrollLeft = pos.left;
36
+ if (!key || !positions.has(key)) {
37
+ return;
39
38
  }
39
+ const pos = positions.get(key);
40
+ el.scrollTop = pos.top;
41
+ el.scrollLeft = pos.left;
42
+ restoredKeys.add(key);
40
43
  });
41
- currentScrollSnapshot = null;
44
+ if (restoredKeys.size === positions.size || remainingAttempts <= 1) {
45
+ currentScrollSnapshot = null;
46
+ return;
47
+ }
48
+ setTimeout(() => restore(remainingAttempts - 1), 50);
42
49
  });
43
- });
50
+ };
51
+ restore(20);
44
52
  }
45
53
  function getScrollPositions(url) {
46
54
  return urlScrollStore.get(url);
package/CHANGELOG.md DELETED
@@ -1,19 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to `@ecopages/react-router` are documented here.
4
-
5
- > **Note:** Changelog tracking begins at version `0.2.0`. Changes prior to this release are not recorded here but are available in the git history.
6
-
7
- ## [UNRELEASED] — TBD
8
-
9
- ### Bug Fixes
10
-
11
- - Fixed React-to-non-React handoffs to replay queued clicks through the next active runtime and reuse prefetched HTML documents instead of forcing a second fetch.
12
- - Fixed stale handoff cleanup and fallback races so older React-router or browser-router navigations cannot overwrite a newer navigation.
13
- - Standardized React route payload reads on `window.__ECO_PAGES__.page` and explicit document owner markers so mixed-router page ownership stays stable.
14
- - Restored current-page HMR refreshes with persist layouts enabled by targeting the active React-router owner.
15
-
16
- ### Refactoring
17
-
18
- - Routed browser handoff and current-page reloads through the shared navigation coordinator.
19
- - Updated package metadata for the current core, esbuild adapter, and React peer dependency surface.
package/browser.ts DELETED
@@ -1,17 +0,0 @@
1
- /**
2
- * Browser entry point for @ecopages/react-router.
3
- * This file exports only the client-side components needed for hydration.
4
- * @module
5
- */
6
-
7
- export { EcoRouter, PageContent } from './src/router.ts';
8
- export type { EcoRouterProps } from './src/router.ts';
9
-
10
- export { useRouter } from './src/context.ts';
11
- export type { RouterContextValue } from './src/context.ts';
12
- export { EcoPropsScript } from './src/props-script.ts';
13
- export type { EcoPropsScriptProps } from './src/props-script.ts';
14
-
15
- export { morphHead } from './src/head-morpher.ts';
16
-
17
- export type { PageState } from './src/navigation.ts';