@ecopages/react-router 0.2.0-alpha.5 → 0.2.0-alpha.50

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,
@@ -11,11 +12,24 @@ import {
11
12
  } from "react";
12
13
  import { DEFAULT_OPTIONS } from "./types.js";
13
14
  import { RouterContext } from "./context.js";
14
- import { getInterceptDecision, loadPageModule, shouldInterceptClick } from "./navigation.js";
15
+ import {
16
+ fetchPageDocument,
17
+ getInterceptDecision,
18
+ isSamePageHashNavigationHref,
19
+ loadPageModuleFromDocument,
20
+ shouldInterceptClick
21
+ } from "./navigation.js";
15
22
  import { morphHead } from "./head-morpher.js";
16
23
  import { applyViewTransitionNames } from "./view-transition-utils.js";
17
24
  import { manageScroll } from "./manage-scroll.js";
18
25
  import { saveScrollPositions, restoreScrollPositions } from "./scroll-persist.js";
26
+ import {
27
+ getEcoNavigationRuntime
28
+ } from "@ecopages/core/router/navigation-coordinator";
29
+ import {
30
+ getAnchorFromNavigationEvent,
31
+ recoverPendingNavigationHref
32
+ } from "@ecopages/core/router/link-intent";
19
33
  const PageContext = createContext(null);
20
34
  const PersistLayoutsContext = createContext(false);
21
35
  function getLayoutFromPage(Page) {
@@ -54,9 +68,10 @@ const PageContent = () => {
54
68
  }
55
69
  return null;
56
70
  }
57
- const { Component: Page, props } = pageContext;
71
+ const { Component: Page, props, refreshPersistedLayout } = pageContext;
58
72
  const Layout = getLayoutFromPage(Page);
59
73
  const pageElement = createElement(Page, props);
74
+ const layoutProps = props?.locals ? { locals: props.locals } : null;
60
75
  if (!Layout) {
61
76
  return pageElement;
62
77
  }
@@ -65,13 +80,13 @@ const PageContent = () => {
65
80
  const layoutConfig = Layout.config;
66
81
  const layoutKeyRaw = layoutConfig?.__eco?.id || Layout.displayName || Layout.name || "layout";
67
82
  const layoutKey = normalizeLayoutKey(layoutKeyRaw);
68
- if (!layoutCache.has(layoutKey)) {
83
+ if (!layoutCache.has(layoutKey) || refreshPersistedLayout && layoutCache.get(layoutKey) !== Layout) {
69
84
  layoutCache.set(layoutKey, Layout);
70
85
  }
71
86
  const CachedLayout = layoutCache.get(layoutKey);
72
- return createElement(CachedLayout, { key: layoutKey }, pageElement);
87
+ return createElement(CachedLayout, { key: layoutKey, ...layoutProps ?? {} }, pageElement);
73
88
  }
74
- return createElement(Layout, null, pageElement);
89
+ return createElement(Layout, layoutProps, pageElement);
75
90
  };
76
91
  function createDeferred() {
77
92
  let resolve;
@@ -80,46 +95,86 @@ function createDeferred() {
80
95
  });
81
96
  return { promise, resolve };
82
97
  }
83
- function useHmrReload(navigate) {
98
+ function useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef) {
99
+ const handleCoordinatorNavigate = useEffectEvent(async (request) => {
100
+ await navigate(request.href, {
101
+ isPopState: request.direction === "back",
102
+ pushHistory: request.direction === "forward",
103
+ skipViewTransition: request.source === "browser-router"
104
+ });
105
+ return true;
106
+ });
107
+ const handleCoordinatorReload = useEffectEvent(async (request) => {
108
+ if (activeNavigationRef.current || isNavigatingRef.current) {
109
+ return;
110
+ }
111
+ if (request?.clearCache) {
112
+ clearLayoutCache();
113
+ }
114
+ const currentUrl = window.location.pathname + window.location.search;
115
+ await navigate(currentUrl, { moduleUrlOverride: request?.moduleUrl });
116
+ });
117
+ const handleCleanupBeforeHandoff = useEffectEvent(async () => {
118
+ runtimeActiveRef.current = false;
119
+ window.__ECO_PAGES__?.react?.cleanupPageRoot?.();
120
+ });
84
121
  useEffect(() => {
85
- if (typeof window === "undefined") return;
86
- if (import.meta.env?.MODE === "production" || import.meta.env?.PROD) return;
87
- const windowWithHmr = window;
88
- windowWithHmr.__ecopages_reload_current_page__ = async (options) => {
89
- if (options?.clearCache) {
90
- clearLayoutCache();
122
+ const navigationRuntime = getEcoNavigationRuntime(window);
123
+ let unregisterRuntime = null;
124
+ const unregister = navigationRuntime.register({
125
+ owner: "react-router",
126
+ navigate: handleCoordinatorNavigate,
127
+ reloadCurrentPage: handleCoordinatorReload,
128
+ cleanupBeforeHandoff: async () => {
129
+ unregisterRuntime?.();
130
+ unregisterRuntime = null;
131
+ await handleCleanupBeforeHandoff();
91
132
  }
92
- const currentUrl = window.location.pathname + window.location.search;
93
- await navigate(currentUrl);
94
- };
133
+ });
134
+ unregisterRuntime = unregister;
135
+ navigationRuntime.claimOwnership("react-router");
136
+ runtimeActiveRef.current = true;
95
137
  return () => {
96
- windowWithHmr.__ecopages_reload_current_page__ = void 0;
138
+ runtimeActiveRef.current = false;
139
+ navigationRuntime.releaseOwnership("react-router");
140
+ unregisterRuntime?.();
141
+ unregisterRuntime = null;
97
142
  };
98
- }, [navigate]);
143
+ }, [runtimeActiveRef]);
99
144
  }
100
145
  const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
101
146
  const options = useMemo(() => ({ ...DEFAULT_OPTIONS, ...userOptions }), [userOptions]);
102
- const [currentPage, setCurrentPage] = useState({ Component: page, props: pageProps });
147
+ const [currentPage, setCurrentPage] = useState({
148
+ Component: page,
149
+ props: pageProps,
150
+ refreshPersistedLayout: false
151
+ });
103
152
  const [isNavigating, setIsNavigating] = useState(false);
104
- const [pendingPage, setPendingPage] = useState(null);
105
- const renderDfd = useRef(null);
106
- const pendingScrollRestoreRef = useRef(null);
153
+ const pendingRenderRef = useRef(null);
154
+ const activeNavigationRef = useRef(null);
155
+ const isNavigatingRef = useRef(false);
156
+ const runtimeActiveRef = useRef(true);
157
+ const pendingPointerNavigationRef = useRef(null);
158
+ const pendingHoverNavigationRef = useRef(null);
159
+ const queuedNavigationHrefRef = useRef(null);
160
+ const committedPathRef = useRef(
161
+ typeof window !== "undefined" ? window.location.pathname + window.location.search : ""
162
+ );
107
163
  const previousUrlRef = useRef(typeof window !== "undefined" ? window.location.href : "");
108
164
  useEffect(() => {
109
- setCurrentPage({ Component: page, props: pageProps });
165
+ isNavigatingRef.current = isNavigating;
166
+ }, [isNavigating]);
167
+ useEffect(() => {
168
+ setCurrentPage({ Component: page, props: pageProps, refreshPersistedLayout: true });
110
169
  }, [page, pageProps]);
111
170
  useEffect(() => {
171
+ committedPathRef.current = window.location.pathname + window.location.search;
112
172
  applyViewTransitionNames();
113
- }, [currentPage]);
114
- useEffect(() => {
115
- if (pendingPage && currentPage.Component === pendingPage.Component && renderDfd.current) {
116
- renderDfd.current.resolve();
117
- renderDfd.current = null;
118
- setPendingPage(null);
173
+ const pendingRender = pendingRenderRef.current;
174
+ if (pendingRender && currentPage.Component === pendingRender.page.Component && currentPage.props === pendingRender.page.props) {
175
+ pendingRender.resolve();
176
+ pendingRenderRef.current = null;
119
177
  }
120
- }, [currentPage, pendingPage]);
121
- useEffect(() => {
122
- if (typeof window === "undefined") return;
123
178
  const url = new URL(window.location.href);
124
179
  const previousUrl = new URL(previousUrlRef.current);
125
180
  if (url.href !== previousUrl.href) {
@@ -129,85 +184,313 @@ const EcoRouter = ({ page, pageProps, options: userOptions, children }) => {
129
184
  });
130
185
  previousUrlRef.current = url.href;
131
186
  }
132
- if (pendingScrollRestoreRef.current) {
133
- const { url: targetUrl, isPopState } = pendingScrollRestoreRef.current;
134
- restoreScrollPositions(targetUrl, isPopState);
135
- pendingScrollRestoreRef.current = null;
136
- }
137
187
  }, [currentPage, options.scrollBehavior, options.smoothScroll]);
188
+ useEffect(() => {
189
+ return () => {
190
+ activeNavigationRef.current?.cancel();
191
+ pendingRenderRef.current?.resolve();
192
+ pendingRenderRef.current = null;
193
+ queuedNavigationHrefRef.current = null;
194
+ };
195
+ }, []);
138
196
  const navigate = useCallback(
139
- async (url, isPopState = false) => {
140
- setIsNavigating(true);
141
- const result = await loadPageModule(url);
142
- if (result) {
143
- const { Component, props, doc, finalPath } = result;
144
- const nextPage = { Component, props };
145
- const cleanupHead = await morphHead(doc);
146
- applyViewTransitionNames();
147
- if (finalPath !== url) {
148
- window.history.replaceState(null, "", finalPath);
197
+ async (url, navigationOptions = {}) => {
198
+ const {
199
+ isPopState = false,
200
+ pushHistory = false,
201
+ skipViewTransition = false,
202
+ moduleUrlOverride
203
+ } = navigationOptions;
204
+ const navigationRuntime = getEcoNavigationRuntime(window);
205
+ const navigation = navigationRuntime.beginNavigationTransaction();
206
+ activeNavigationRef.current = navigation;
207
+ const navigationId = navigation.id;
208
+ const isStale = () => !navigation.isCurrent();
209
+ const commitPageData = (moduleUrl, props) => {
210
+ window.__ECO_PAGES__ = window.__ECO_PAGES__ || {};
211
+ window.__ECO_PAGES__.page = {
212
+ module: moduleUrl,
213
+ props
214
+ };
215
+ };
216
+ const preparePendingRender = (nextPage) => {
217
+ pendingRenderRef.current?.resolve();
218
+ const renderDfd = createDeferred();
219
+ pendingRenderRef.current = {
220
+ navigationId,
221
+ page: nextPage,
222
+ resolve: renderDfd.resolve
223
+ };
224
+ return renderDfd.promise;
225
+ };
226
+ let navigationCommitPromise = null;
227
+ try {
228
+ setIsNavigating(true);
229
+ const fetchedPage = await fetchPageDocument(url, { signal: navigation.signal });
230
+ if (isStale()) return;
231
+ if (!fetchedPage) {
232
+ window.location.href = url;
233
+ return;
149
234
  }
150
- saveScrollPositions();
151
- pendingScrollRestoreRef.current = { url, isPopState };
152
- if (options.viewTransitions && document.startViewTransition) {
153
- renderDfd.current = createDeferred();
154
- setPendingPage(nextPage);
155
- document.startViewTransition(async () => {
156
- startTransition(() => {
157
- setCurrentPage(nextPage);
158
- });
159
- await renderDfd.current?.promise;
235
+ const result = await loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath, {
236
+ moduleUrlOverride
237
+ });
238
+ if (isStale()) return;
239
+ if (result) {
240
+ const { Component, props, doc, finalPath, moduleUrl } = result;
241
+ const nextPage = { Component, props, refreshPersistedLayout: Boolean(moduleUrlOverride) };
242
+ const { cleanup: cleanupHead, flushRerunScripts } = await morphHead(doc);
243
+ const finalizeCommittedNavigation = () => {
244
+ committedPathRef.current = finalPath;
245
+ flushRerunScripts();
160
246
  cleanupHead();
161
247
  applyViewTransitionNames();
162
- });
163
- } else {
164
- setCurrentPage(nextPage);
165
- cleanupHead();
248
+ restoreScrollPositions(finalPath, isPopState);
249
+ };
250
+ const commitNextPage = () => {
251
+ commitPageData(moduleUrl, props);
252
+ setCurrentPage(nextPage);
253
+ };
254
+ if (isStale()) {
255
+ cleanupHead();
256
+ return;
257
+ }
166
258
  applyViewTransitionNames();
259
+ saveScrollPositions();
260
+ if (pushHistory) {
261
+ window.history.pushState(null, "", finalPath);
262
+ } else if (finalPath !== url) {
263
+ window.history.replaceState(null, "", finalPath);
264
+ }
265
+ if (!skipViewTransition && options.viewTransitions && document.startViewTransition) {
266
+ const renderPromise = preparePendingRender(nextPage);
267
+ navigationCommitPromise = new Promise((resolve) => {
268
+ document.startViewTransition(async () => {
269
+ try {
270
+ if (isStale()) {
271
+ if (pendingRenderRef.current?.navigationId === navigationId) {
272
+ pendingRenderRef.current.resolve();
273
+ pendingRenderRef.current = null;
274
+ }
275
+ cleanupHead();
276
+ return;
277
+ }
278
+ startTransition(() => {
279
+ commitNextPage();
280
+ });
281
+ await renderPromise;
282
+ if (isStale()) {
283
+ return;
284
+ }
285
+ finalizeCommittedNavigation();
286
+ } finally {
287
+ resolve();
288
+ }
289
+ });
290
+ });
291
+ await navigationCommitPromise;
292
+ } else {
293
+ const renderPromise = preparePendingRender(nextPage);
294
+ commitNextPage();
295
+ await renderPromise;
296
+ if (isStale()) {
297
+ cleanupHead();
298
+ return;
299
+ }
300
+ finalizeCommittedNavigation();
301
+ }
302
+ } else {
303
+ if (isStale()) return;
304
+ const handled = await navigationRuntime.requestHandoff({
305
+ href: url,
306
+ finalHref: fetchedPage.finalPath,
307
+ direction: isPopState ? "back" : pushHistory ? "forward" : "replace",
308
+ source: "react-router",
309
+ targetOwner: "browser-router",
310
+ document: fetchedPage.doc,
311
+ html: fetchedPage.html,
312
+ isStaleSourceNavigation: isStale
313
+ });
314
+ if (!handled) {
315
+ window.location.assign(fetchedPage.finalPath);
316
+ }
317
+ }
318
+ if (!isStale()) {
319
+ setIsNavigating(false);
167
320
  }
168
- } else {
169
- if (options.debug) {
170
- console.error("[EcoRouter] Falling back to full page navigation:", url);
321
+ } finally {
322
+ const shouldReplayQueuedNavigation = activeNavigationRef.current?.id === navigationId;
323
+ const queuedNavigationHref = shouldReplayQueuedNavigation ? queuedNavigationHrefRef.current : null;
324
+ const queuedNavigationPath = queuedNavigationHref ? new URL(queuedNavigationHref, window.location.origin).pathname + new URL(queuedNavigationHref, window.location.origin).search : null;
325
+ navigation.complete();
326
+ if (activeNavigationRef.current?.id === navigationId) {
327
+ activeNavigationRef.current = null;
328
+ }
329
+ if (queuedNavigationHref && queuedNavigationPath !== committedPathRef.current) {
330
+ queuedNavigationHrefRef.current = null;
331
+ if (runtimeActiveRef.current) {
332
+ void navigate(queuedNavigationHref, { pushHistory: true });
333
+ } else {
334
+ void navigationRuntime.requestNavigation({
335
+ href: queuedNavigationHref,
336
+ direction: "forward",
337
+ source: "react-router"
338
+ }).then((handled) => {
339
+ if (!handled) {
340
+ window.location.assign(queuedNavigationHref);
341
+ }
342
+ });
343
+ }
171
344
  }
172
- window.location.href = url;
173
345
  }
174
- setIsNavigating(false);
175
346
  },
176
- [options.viewTransitions, options.debug]
347
+ [options.viewTransitions]
177
348
  );
178
- useEffect(() => {
179
- const handleClick = (event) => {
180
- const link = event.target.closest(options.linkSelector);
181
- if (!link) return;
182
- if (!shouldInterceptClick(event, link, options)) {
183
- if (options.debug) {
184
- const decision = getInterceptDecision(event, link, options);
185
- if (!decision.shouldIntercept) {
186
- console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
187
- }
188
- }
349
+ const getLinkFromEvent = useEffectEvent(
350
+ (event) => getAnchorFromNavigationEvent(event, options.linkSelector)
351
+ );
352
+ const getRecoveredPointerHref = useEffectEvent(() => {
353
+ const href = recoverPendingNavigationHref(
354
+ pendingPointerNavigationRef.current,
355
+ !!activeNavigationRef.current || isNavigatingRef.current,
356
+ performance.now()
357
+ );
358
+ if (!href) {
359
+ pendingPointerNavigationRef.current = null;
360
+ }
361
+ return href;
362
+ });
363
+ const getRecoveredHoverHref = useEffectEvent(() => {
364
+ const href = recoverPendingNavigationHref(
365
+ pendingHoverNavigationRef.current,
366
+ !!activeNavigationRef.current || isNavigatingRef.current,
367
+ performance.now()
368
+ );
369
+ if (!href) {
370
+ pendingHoverNavigationRef.current = null;
371
+ }
372
+ return href;
373
+ });
374
+ const handleHoverIntent = useEffectEvent((event) => {
375
+ if (!runtimeActiveRef.current) {
376
+ return;
377
+ }
378
+ const link = getLinkFromEvent(event);
379
+ if (!link) {
380
+ return;
381
+ }
382
+ const decision = getInterceptDecision(event, link, options);
383
+ if (!decision.shouldIntercept) {
384
+ return;
385
+ }
386
+ pendingHoverNavigationRef.current = {
387
+ href: link.getAttribute("href"),
388
+ timestamp: performance.now()
389
+ };
390
+ queuedNavigationHrefRef.current = link.getAttribute("href");
391
+ });
392
+ const handlePointerDown = useEffectEvent((event) => {
393
+ if (!runtimeActiveRef.current) {
394
+ pendingPointerNavigationRef.current = null;
395
+ return;
396
+ }
397
+ const link = getLinkFromEvent(event);
398
+ if (!link) {
399
+ pendingPointerNavigationRef.current = null;
400
+ return;
401
+ }
402
+ const decision = getInterceptDecision(event, link, options);
403
+ pendingPointerNavigationRef.current = decision.shouldIntercept ? {
404
+ href: link.getAttribute("href"),
405
+ timestamp: performance.now()
406
+ } : null;
407
+ if (decision.shouldIntercept && (activeNavigationRef.current || isNavigatingRef.current)) {
408
+ queuedNavigationHrefRef.current = link.getAttribute("href");
409
+ }
410
+ });
411
+ const handleClick = useEffectEvent((event) => {
412
+ if (!runtimeActiveRef.current) {
413
+ pendingPointerNavigationRef.current = null;
414
+ pendingHoverNavigationRef.current = null;
415
+ return;
416
+ }
417
+ const link = getLinkFromEvent(event);
418
+ if (!link) {
419
+ const recoveredHref = getRecoveredPointerHref() ?? getRecoveredHoverHref();
420
+ pendingPointerNavigationRef.current = null;
421
+ pendingHoverNavigationRef.current = null;
422
+ if (!recoveredHref) {
423
+ return;
424
+ }
425
+ if (isSamePageHashNavigationHref(recoveredHref)) {
426
+ queuedNavigationHrefRef.current = null;
189
427
  return;
190
428
  }
191
429
  event.preventDefault();
192
- const href = link.getAttribute("href");
193
- const url = new URL(href, window.location.origin);
430
+ queuedNavigationHrefRef.current = null;
431
+ const recoveredUrl = new URL(recoveredHref, window.location.href);
432
+ navigate(recoveredUrl.pathname + recoveredUrl.search, { pushHistory: true });
433
+ return;
434
+ }
435
+ if (!shouldInterceptClick(event, link, options)) {
194
436
  if (options.debug) {
195
- console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
437
+ const decision = getInterceptDecision(event, link, options);
438
+ if (!decision.shouldIntercept) {
439
+ console.debug("[EcoRouter] Not intercepting link click:", decision.reason, link.href);
440
+ }
196
441
  }
197
- window.history.pushState(null, "", url.href);
198
- navigate(url.pathname + url.search);
442
+ pendingPointerNavigationRef.current = null;
443
+ pendingHoverNavigationRef.current = null;
444
+ return;
445
+ }
446
+ pendingPointerNavigationRef.current = null;
447
+ pendingHoverNavigationRef.current = null;
448
+ event.preventDefault();
449
+ queuedNavigationHrefRef.current = null;
450
+ const href = link.getAttribute("href");
451
+ const url = new URL(href, window.location.origin);
452
+ if (options.debug) {
453
+ console.debug("[EcoRouter] Intercepting navigation:", url.pathname + url.search);
454
+ }
455
+ navigate(url.pathname + url.search, { pushHistory: true });
456
+ });
457
+ const handlePopState = useEffectEvent(() => {
458
+ if (!runtimeActiveRef.current) {
459
+ return;
460
+ }
461
+ navigate(window.location.pathname + window.location.search, { isPopState: true });
462
+ });
463
+ useEffect(() => {
464
+ const onHoverIntent = (event) => {
465
+ handleHoverIntent(event);
466
+ };
467
+ const onPointerDown = (event) => {
468
+ handlePointerDown(event);
469
+ };
470
+ const onClick = (event) => {
471
+ handleClick(event);
199
472
  };
200
- const handlePopState = () => {
201
- navigate(window.location.pathname + window.location.search, true);
473
+ const onPopState = () => {
474
+ handlePopState();
202
475
  };
203
- document.addEventListener("click", handleClick);
204
- window.addEventListener("popstate", handlePopState);
476
+ document.addEventListener("mouseover", onHoverIntent, true);
477
+ document.addEventListener("pointerover", onHoverIntent, true);
478
+ document.addEventListener("mousemove", onHoverIntent, true);
479
+ document.addEventListener("pointermove", onHoverIntent, true);
480
+ document.addEventListener("pointerdown", onPointerDown, true);
481
+ document.addEventListener("click", onClick, true);
482
+ window.addEventListener("popstate", onPopState);
205
483
  return () => {
206
- document.removeEventListener("click", handleClick);
207
- window.removeEventListener("popstate", handlePopState);
484
+ document.removeEventListener("mouseover", onHoverIntent, true);
485
+ document.removeEventListener("pointerover", onHoverIntent, true);
486
+ document.removeEventListener("mousemove", onHoverIntent, true);
487
+ document.removeEventListener("pointermove", onHoverIntent, true);
488
+ document.removeEventListener("pointerdown", onPointerDown, true);
489
+ document.removeEventListener("click", onClick, true);
490
+ window.removeEventListener("popstate", onPopState);
208
491
  };
209
- }, [navigate, options]);
210
- useHmrReload(navigate);
492
+ }, []);
493
+ useNavigationCoordinator(navigate, activeNavigationRef, isNavigatingRef, runtimeActiveRef);
211
494
  return createElement(
212
495
  RouterContext.Provider,
213
496
  { value: { navigate, isNavigating } },
@@ -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,12 +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
- ### Refactoring
10
-
11
- - Updated `package.json` dependencies to align with the new core adapter and esbuild build adapter versions.
12
- - Internal peer dependency declarations updated for React 18+ and the new `@ecopages/core` API 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';
package/src/adapter.ts DELETED
@@ -1,48 +0,0 @@
1
- /**
2
- * Router adapter for React integration.
3
- * @module
4
- */
5
-
6
- import type { ReactRouterAdapter } from '@ecopages/react/router-adapter';
7
- import type { EcoRouterOptions } from './types.ts';
8
-
9
- /**
10
- * Creates a ReactRouterAdapter for EcoPages React Router.
11
- * Use this with the React plugin to enable SPA navigation.
12
- *
13
- * @param options - Router configuration options
14
- * @example
15
- * ```ts
16
- * import { reactPlugin } from '@ecopages/react';
17
- * import { ecoRouter } from '@ecopages/react-router';
18
- *
19
- * export default {
20
- * integrations: [reactPlugin({ router: ecoRouter() })],
21
- * };
22
- * ```
23
- *
24
- * @example
25
- * ```ts
26
- * // Disable view transitions
27
- * reactPlugin({ router: ecoRouter({ viewTransitions: false }) })
28
- * ```
29
- */
30
- export function ecoRouter(options?: EcoRouterOptions): ReactRouterAdapter {
31
- return {
32
- name: 'eco-router',
33
- bundle: {
34
- importPath: '@ecopages/react-router/browser.ts',
35
- outputName: 'react-router-esm',
36
- externals: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime'],
37
- },
38
- importMapKey: '@ecopages/react-router',
39
- components: {
40
- router: 'EcoRouter',
41
- pageContent: 'PageContent',
42
- },
43
- getRouterProps(page: string, props: string): string {
44
- const optionsStr = options ? `, options: ${JSON.stringify(options)}` : '';
45
- return `{ page: ${page}, pageProps: ${props}${optionsStr} }`;
46
- },
47
- };
48
- }