@ecopages/react-router 0.2.0-alpha.3 → 0.2.0-alpha.31

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/CHANGELOG.md CHANGED
@@ -6,7 +6,16 @@ All notable changes to `@ecopages/react-router` are documented here.
6
6
 
7
7
  ## [UNRELEASED] — TBD
8
8
 
9
+ ### Bug Fixes
10
+
11
+ - Fixed same-page hash links and Shadow DOM TOC clicks to bypass React Router interception so anchor navigation preserves the URL fragment without a document fetch.
12
+ - Extended page-module extraction to honor explicit hydration markers and self-owned React page entry bundles during navigation.
13
+ - Fixed current-page reloads to accept HMR module overrides so persisted-layout refreshes import the rebuilt active page entry.
14
+ - Fixed React-to-browser-router handoffs, queued-click replay, and stale-navigation races during mixed-router navigations.
15
+ - Standardized route payload reads, document-owner markers, rerun scripts, and current-page HMR refreshes for persisted React layouts.
16
+
9
17
  ### Refactoring
10
18
 
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.
19
+ - Routed browser handoff and current-page reloads through the shared navigation coordinator.
20
+ - Removed the React router adapter `importMapKey` field so the adapter now exposes only the browser bundle import path used by both development and production hydration.
21
+ - Updated package metadata for the current core, esbuild adapter, and React peer dependency surface.
package/README.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # @ecopages/react-router
2
2
 
3
- Client-side SPA router for EcoPages React applications. Enables single-page application navigation while preserving full SSR benefits.
3
+ Client-side SPA router for Ecopages React applications. Features single-page application navigation while preserving all the benefits of Server-Side Rendering (SSR).
4
+
5
+ ## Features
6
+
7
+ - **SSR preserved**: Initial loads are fully server-rendered.
8
+ - **Opt-in via config**: A single line in your config enables SPA navigation across all pages.
9
+ - **Layout persistence**: Shared layouts stay mounted while page content swaps.
10
+ - **Standard links**: Works with regular `<a>` tags.
11
+ - **Head sync**: Automatically updates document metadata `<head>` during navigation.
12
+ - **View Transitions**: Built-in support for the browser View Transitions API.
13
+
14
+ ## Cross-Runtime Handoff
15
+
16
+ `@ecopages/react-router` only performs SPA updates for React-managed documents. When a navigation resolves to a non-React document, it will:
17
+
18
+ - hand the already-fetched HTML document to `@ecopages/browser-router` when browser-router is registered on the page
19
+ - fall back to a normal document navigation when browser-router is not present
20
+
21
+ This keeps React-router focused on React rendering while still allowing mixed React and non-React pages to transition without a second fetch when browser-router is active.
4
22
 
5
23
  ## Installation
6
24
 
@@ -10,37 +28,30 @@ bun add @ecopages/react-router
10
28
 
11
29
  ## Quick Start
12
30
 
13
- Add the router adapter to your `eco.config.ts`:
31
+ Pass the router adapter to the React plugin in your `eco.config.ts`:
14
32
 
15
33
  ```typescript
16
- import { ConfigBuilder } from '@ecopages/core';
34
+ import { ConfigBuilder } from '@ecopages/core/config-builder';
17
35
  import { reactPlugin } from '@ecopages/react';
18
36
  import { ecoRouter } from '@ecopages/react-router';
19
37
 
20
38
  const config = await new ConfigBuilder()
21
- .setRootDir(import.meta.dir)
39
+ .setRootDir(import.meta.dirname)
22
40
  .setIntegrations([reactPlugin({ router: ecoRouter() })])
23
41
  .build();
24
42
 
25
43
  export default config;
26
44
  ```
27
45
 
28
- That's it! All pages now have SPA navigation enabled.
29
-
30
- ## Features
46
+ SPA navigation is now enabled for all React pages in your project.
31
47
 
32
- - **Opt-in via config** - Single line enables SPA for all pages
33
- - **SSR preserved** - Full server-side rendering on initial load
34
- - **Layout persistence** - Layouts stay mounted, only page content swaps
35
- - **Standard links** - Works with regular `<a>` tags
36
- - **Head sync** - Automatically updates title, meta, and stylesheets
37
- - **Pluggable** - Extensible adapter pattern
48
+ If your site mixes React pages with non-React pages, you can also run `@ecopages/browser-router` on the non-React shell. React-router will hand off non-React navigations to browser-router when it is available.
38
49
 
39
50
  ## Usage
40
51
 
41
52
  ### Layouts (Optional)
42
53
 
43
- Use `config.layout` for persistent UI across navigations:
54
+ Configure your page with a layout to keep UI components (like headers/navs) mounted across navigations:
44
55
 
45
56
  ```tsx
46
57
  // src/layouts/base-layout.tsx
@@ -65,6 +76,8 @@ export default HomePage;
65
76
 
66
77
  ### Links
67
78
 
79
+ Standard relative links are intercepted natively. To bypass the router and force a hard reload, use the `data-eco-reload` attribute.
80
+
68
81
  ```tsx
69
82
  // SPA navigation (intercepted)
70
83
  <a href="/about">About</a>
@@ -89,39 +102,21 @@ const MyComponent = () => {
89
102
  };
90
103
  ```
91
104
 
92
- ### View Transitions
93
-
94
- The router automatically supports the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API) for smooth page transitions.
105
+ ## View Transitions
95
106
 
96
- #### Lifecycle
107
+ The router automatically integrates with the [View Transitions API](https://developer.mozilla.org/en-US/docs/Web/API/View_Transitions_API).
97
108
 
98
- When a navigation occurs with View Transitions enabled:
99
-
100
- 1. **Snapshot**: The browser captures the current state (screenshot) of the page.
101
- 2. **Update**: React processes the state change and renders the new page.
102
- 3. **Animate**: The browser animates from the old snapshot to the new live state.
103
-
104
- The router uses a deferred promise mechanism to ensure React has fully finished rendering the new content before telling the browser to start the animation phase.
105
-
106
- #### Shared Element Transitions
107
-
108
- To animate elements between pages (e.g., a thumbnail becoming a hero image), use the `data-view-transition` attribute. Ensure the value is unique to the specific element being transitioned and matches on both pages.
109
+ To animate elements between pages using Shared Element Transitions, mark them with a unique `data-view-transition` id that matches across both pages:
109
110
 
110
111
  ```tsx
111
- // List Page (Source)
112
- <img
113
- src={post.image}
114
- data-view-transition={`hero-${post.id}`}
115
- />
116
-
117
- // Detail Page (Destination)
118
- <img
119
- src={post.image}
120
- data-view-transition={`hero-${post.id}`}
121
- />
112
+ // List Page
113
+ <img src={post.image} data-view-transition={`hero-${post.id}`} />
114
+
115
+ // Detail Page
116
+ <img src={post.image} data-view-transition={`hero-${post.id}`} />
122
117
  ```
123
118
 
124
- By default, the router applies a **clean morph** animation (disabling the default cross-fade ghosting). If you prefer the standard browser cross-fade, you can opt-out:
119
+ By default, we impose a "clean morph", disabling default cross-fade ghosting. To use standard crossfades on elements, opt-out:
125
120
 
126
121
  ```tsx
127
122
  <div data-view-transition="my-hero" data-view-transition-animate="fade">
@@ -129,84 +124,15 @@ By default, the router applies a **clean morph** animation (disabling the defaul
129
124
  </div>
130
125
  ```
131
126
 
132
- #### Cross-Fade
133
-
134
- By default, the router provides a smooth cross-fade for the root content. You can customize this by overriding the default view transition CSS:
135
-
136
- ```css
137
- ::view-transition-old(root),
138
- ::view-transition-new(root) {
139
- animation-duration: 0.5s;
140
- }
141
- ```
142
-
143
127
  ## How It Works
144
128
 
145
- The router uses an **HTML-First** navigation strategy to ensure consistency with Server-Side Rendering (SSR).
146
-
147
- 1. **SSR**: Server renders full HTML for the initial page load.
148
- 2. **Hydration**: Client hydrates, router attaches to the document.
149
- 3. **Navigation**: On link click:
150
- - **Fetch**: Requests the full HTML of the target page (just like a standard browser navigation).
151
- - **Parse**: Extracts the page component URL and serialized props from the HTML.
152
- - **Preload**: Dynamically imports the new page component.
153
- - **Transition**:
154
- - Calls `document.startViewTransition()`.
155
- - Updates the document head (title, meta, styles).
156
- - Updates the React state to render the new page component.
157
- - Waits for React commit (useEffect).
158
- - **Resolve**: View Transition finishes, browser plays the animation.
159
-
160
- ## API
161
-
162
- ### `ecoRouter()`
163
-
164
- Creates a router adapter for the React plugin.
165
-
166
- ```typescript
167
- reactPlugin({ router: ecoRouter() });
168
- ```
169
-
170
- ### `useRouter()`
171
-
172
- Hook for programmatic navigation.
173
-
174
- ```typescript
175
- const { navigate, isPending } = useRouter();
176
- ```
177
-
178
- ### Link Behavior
179
-
180
- Links are **not** intercepted when:
181
-
182
- - Modifier keys held (Ctrl, Cmd, Shift, Alt)
183
- - Has `target="_blank"` or `download` attribute
184
- - Has `data-eco-reload` attribute
185
- - Points to different origin
186
- - Starts with `#` or `javascript:`
187
-
188
- ## Architecture
189
-
190
- The router uses a pluggable adapter pattern:
191
-
192
- ```typescript
193
- interface ReactRouterAdapter {
194
- name: string;
195
- bundle: { importPath; outputName; externals };
196
- importMapKey: string;
197
- components: { router; pageContent };
198
- getRouterProps(page, props): string;
199
- }
200
- ```
201
-
202
- This allows custom router implementations while keeping integration simple.
203
-
204
- ## Compatibility
205
-
206
- - React 18.x or 19.x
207
- - Modern browsers with ES modules
208
- - EcoPages with React integration
209
-
210
- ## License
129
+ The router relies on **HTML-First** navigation to sync perfectly with SSR:
211
130
 
212
- MIT
131
+ 1. **SSR**: Initial page arrives completely rendered.
132
+ 2. **Hydration**: Client hydrates and the router attaches.
133
+ 3. **Navigation**: On click, the router:
134
+ - Fetches the raw HTML of the next route.
135
+ - Extracts page-level serialized props and metadata.
136
+ - Preloads the next page component via dynamic import.
137
+ - Updates React state, syncs `<head>`, and triggers `startViewTransition`.
138
+ - The React graph reconciles and the animation plays.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/react-router",
3
- "version": "0.2.0-alpha.3",
3
+ "version": "0.2.0-alpha.31",
4
4
  "description": "Client-side SPA router for EcoPages React applications",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -32,7 +32,8 @@
32
32
  "directory": "packages/react-router"
33
33
  },
34
34
  "peerDependencies": {
35
- "@ecopages/react": "0.2.0-alpha.3"
35
+ "@ecopages/core": "0.2.0-alpha.31",
36
+ "@ecopages/react": "0.2.0-alpha.31"
36
37
  },
37
38
  "dependencies": {
38
39
  "react": "^19",
package/src/adapter.js CHANGED
@@ -2,11 +2,10 @@ function ecoRouter(options) {
2
2
  return {
3
3
  name: "eco-router",
4
4
  bundle: {
5
- importPath: "@ecopages/react-router/browser.ts",
5
+ importPath: "@ecopages/react-router/browser",
6
6
  outputName: "react-router-esm",
7
7
  externals: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime"]
8
8
  },
9
- importMapKey: "@ecopages/react-router",
10
9
  components: {
11
10
  router: "EcoRouter",
12
11
  pageContent: "PageContent"
@@ -3,6 +3,10 @@
3
3
  * Intelligently syncs head elements between pages using key-based diffing.
4
4
  * @module
5
5
  */
6
+ export type HeadMorphResult = {
7
+ cleanup: () => void;
8
+ flushRerunScripts: () => void;
9
+ };
6
10
  /**
7
11
  * Morphs the current document head to match the new document's head.
8
12
  * Now splits the process into adding new elements and returning a cleanup function
@@ -10,6 +14,6 @@
10
14
  * don't disappear before the "old" snapshot is taken.
11
15
  *
12
16
  * @param newDocument - The parsed document from the navigation target
13
- * @returns Promise that resolves to a cleanup function when new stylesheets have loaded
17
+ * @returns Promise that resolves to cleanup and rerun hooks when new stylesheets have loaded
14
18
  */
15
- export declare function morphHead(newDocument: Document): Promise<() => void>;
19
+ export declare function morphHead(newDocument: Document): Promise<HeadMorphResult>;
@@ -1,4 +1,50 @@
1
- const PRESERVE_SELECTORS = ['script[type="importmap"]', "meta[charset]", "[data-eco-persist]"];
1
+ import { isReactRouterPageBootstrapAssetSrc } from "./hydration-assets.js";
2
+ const PRESERVE_SELECTORS = ["meta[charset]", "[data-eco-persist]"];
3
+ const RERUN_SRC_ATTR = "data-eco-rerun-src";
4
+ let rerunNonce = 0;
5
+ function isNonExecutableHeadScript(el) {
6
+ if (el.tagName !== "SCRIPT") {
7
+ return false;
8
+ }
9
+ const type = (el.getAttribute("type") ?? "").trim().toLowerCase();
10
+ if (!type) {
11
+ return false;
12
+ }
13
+ return ![
14
+ "application/javascript",
15
+ "application/ecmascript",
16
+ "module",
17
+ "text/ecmascript",
18
+ "text/javascript"
19
+ ].includes(type);
20
+ }
21
+ function shouldPersistExecutableInlineHeadScript(el) {
22
+ if (el.tagName !== "SCRIPT") {
23
+ return false;
24
+ }
25
+ const scriptId = el.getAttribute("data-eco-script-id") || el.getAttribute("id");
26
+ if (!scriptId) {
27
+ return false;
28
+ }
29
+ if (el.hasAttribute("data-eco-rerun")) {
30
+ return false;
31
+ }
32
+ if (el.src) {
33
+ return false;
34
+ }
35
+ return !isNonExecutableHeadScript(el);
36
+ }
37
+ function isRerunScript(el) {
38
+ return el.tagName === "SCRIPT" && el.hasAttribute("data-eco-rerun");
39
+ }
40
+ function isReactRouterPageBootstrapScriptId(scriptId) {
41
+ return !!scriptId && scriptId.startsWith("ecopages-react-") && !scriptId.startsWith("ecopages-react-island-");
42
+ }
43
+ function isHydrationScript(el) {
44
+ const src = el.getAttribute("src");
45
+ const scriptId = el.getAttribute("data-eco-script-id");
46
+ return isReactRouterPageBootstrapScriptId(scriptId) || !!src && isReactRouterPageBootstrapAssetSrc(src);
47
+ }
2
48
  function getHeadElementKey(el) {
3
49
  const tag = el.tagName.toLowerCase();
4
50
  switch (tag) {
@@ -17,8 +63,9 @@ function getHeadElementKey(el) {
17
63
  return href ? `link:${href}` : null;
18
64
  }
19
65
  case "script": {
20
- if (el.getAttribute("type") === "importmap") return "importmap";
21
- const src = el.src;
66
+ const scriptId = el.getAttribute("data-eco-script-id") || el.getAttribute("id");
67
+ if (scriptId) return `script-id:${scriptId}`;
68
+ const src = el.getAttribute(RERUN_SRC_ATTR) || el.src;
22
69
  return src ? `script:${src}` : null;
23
70
  }
24
71
  case "style": {
@@ -36,6 +83,12 @@ async function morphHead(newDocument) {
36
83
  const newElements = /* @__PURE__ */ new Map();
37
84
  const stylesheetPromises = [];
38
85
  const elementsToRemove = [];
86
+ const pendingRerunScripts = Array.from(newHead.querySelectorAll("script[data-eco-rerun]")).filter((script) => !isHydrationScript(script)).map((script) => ({
87
+ attributes: Array.from(script.attributes).map((attr) => [attr.name, attr.value]),
88
+ textContent: script.textContent ?? "",
89
+ scriptId: script.getAttribute("data-eco-script-id"),
90
+ src: script.getAttribute("src")
91
+ }));
39
92
  for (const el of Array.from(currentHead.children)) {
40
93
  const key = getHeadElementKey(el);
41
94
  if (key) currentElements.set(key, el);
@@ -46,9 +99,11 @@ async function morphHead(newDocument) {
46
99
  }
47
100
  for (const [key, newEl] of newElements) {
48
101
  const currentEl = currentElements.get(key);
102
+ if (isRerunScript(newEl)) {
103
+ continue;
104
+ }
49
105
  if (!currentEl) {
50
- const src = newEl.getAttribute("src");
51
- if (newEl.tagName === "SCRIPT" && src && src.includes("hydration.js") && src.includes("ecopages-react")) {
106
+ if (newEl.tagName === "SCRIPT" && isHydrationScript(newEl)) {
52
107
  continue;
53
108
  }
54
109
  const cloned = newEl.cloneNode(true);
@@ -62,13 +117,15 @@ async function morphHead(newDocument) {
62
117
  currentHead.appendChild(cloned);
63
118
  } else if (key === "title" && currentEl.textContent !== newEl.textContent) {
64
119
  currentEl.textContent = newEl.textContent;
120
+ } else if (isNonExecutableHeadScript(newEl) && currentEl.textContent !== newEl.textContent) {
121
+ currentEl.textContent = newEl.textContent;
65
122
  } else if (key.startsWith("style:") && currentEl.textContent !== newEl.textContent) {
66
123
  currentEl.textContent = newEl.textContent;
67
124
  }
68
125
  }
69
126
  for (const newEl of Array.from(newHead.children)) {
70
127
  const key = getHeadElementKey(newEl);
71
- if (!key) {
128
+ if (!key && !isRerunScript(newEl)) {
72
129
  currentHead.appendChild(newEl.cloneNode(true));
73
130
  }
74
131
  }
@@ -78,17 +135,60 @@ async function morphHead(newDocument) {
78
135
  for (const [key, el] of currentElements) {
79
136
  if (!newElements.has(key)) {
80
137
  const shouldPreserve = PRESERVE_SELECTORS.some((sel) => el.matches(sel));
81
- if (!shouldPreserve) {
138
+ if (!shouldPreserve && !shouldPersistExecutableInlineHeadScript(el)) {
82
139
  elementsToRemove.push(el);
83
140
  }
84
141
  }
85
142
  }
86
- return () => {
87
- for (const el of elementsToRemove) {
88
- el.remove();
143
+ return {
144
+ cleanup: () => {
145
+ for (const el of elementsToRemove) {
146
+ el.remove();
147
+ }
148
+ },
149
+ flushRerunScripts: () => {
150
+ for (const script of pendingRerunScripts) {
151
+ const replacement = document.createElement("script");
152
+ const shouldBustModuleSrc = isExternalModuleRerunScript(script);
153
+ for (const [name, value] of script.attributes) {
154
+ if (name === "src" && shouldBustModuleSrc) {
155
+ replacement.setAttribute(RERUN_SRC_ATTR, value);
156
+ replacement.setAttribute("src", createRerunScriptUrl(value));
157
+ continue;
158
+ }
159
+ replacement.setAttribute(name, value);
160
+ }
161
+ replacement.textContent = script.textContent;
162
+ const existingScript = findExistingRerunScript(script);
163
+ if (existingScript) {
164
+ existingScript.replaceWith(replacement);
165
+ continue;
166
+ }
167
+ document.head.appendChild(replacement);
168
+ }
89
169
  }
90
170
  };
91
171
  }
172
+ function findExistingRerunScript(script) {
173
+ const scripts = Array.from(document.head.querySelectorAll("script"));
174
+ if (script.scriptId) {
175
+ return scripts.find((candidate) => candidate.getAttribute("data-eco-script-id") === script.scriptId) ?? null;
176
+ }
177
+ return scripts.find(
178
+ (candidate) => (candidate.getAttribute(RERUN_SRC_ATTR) ?? candidate.getAttribute("src")) === script.src && (candidate.textContent ?? "") === script.textContent
179
+ ) ?? null;
180
+ }
181
+ function isExternalModuleRerunScript(script) {
182
+ if (!script.src) {
183
+ return false;
184
+ }
185
+ return script.attributes.some(([name, value]) => name === "type" && value === "module");
186
+ }
187
+ function createRerunScriptUrl(src) {
188
+ const url = new URL(src, document.baseURI);
189
+ url.searchParams.set("__eco_rerun", String(++rerunNonce));
190
+ return url.toString();
191
+ }
92
192
  export {
93
193
  morphHead
94
194
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Returns whether a script URL belongs to a router-managed React page bootstrap asset.
3
+ */
4
+ export declare function isReactRouterPageBootstrapAssetSrc(src: string): boolean;
5
+ /**
6
+ * Returns whether a script URL follows the legacy React hydration asset naming pattern.
7
+ */
8
+ export declare function isReactHydrationAssetSrc(src: string): boolean;
9
+ /**
10
+ * Returns whether a script URL should be treated as the page module source during router navigation.
11
+ */
12
+ export declare function isReactPageHydrationAssetSrc(src: string): boolean;
@@ -0,0 +1,17 @@
1
+ function isReactPageAssetName(src) {
2
+ return src.includes("ecopages-react-") && src.endsWith(".js");
3
+ }
4
+ function isReactRouterPageBootstrapAssetSrc(src) {
5
+ return isReactPageAssetName(src) && !src.includes("ecopages-react-island-");
6
+ }
7
+ function isReactHydrationAssetSrc(src) {
8
+ return isReactPageAssetName(src) && src.includes("hydration.js");
9
+ }
10
+ function isReactPageHydrationAssetSrc(src) {
11
+ return isReactRouterPageBootstrapAssetSrc(src);
12
+ }
13
+ export {
14
+ isReactHydrationAssetSrc,
15
+ isReactPageHydrationAssetSrc,
16
+ isReactRouterPageBootstrapAssetSrc
17
+ };
@@ -8,12 +8,38 @@ export type PageState = {
8
8
  Component: ComponentType<any>;
9
9
  props: Record<string, any>;
10
10
  };
11
+ export type LoadedPageModule = {
12
+ Component: ComponentType<any>;
13
+ props: Record<string, any>;
14
+ doc: Document;
15
+ finalPath: string;
16
+ moduleUrl: string;
17
+ };
18
+ export type FetchedPageDocument = {
19
+ doc: Document;
20
+ finalPath: string;
21
+ html: string;
22
+ };
23
+ type LoadPageModuleOptions = {
24
+ signal?: AbortSignal;
25
+ };
26
+ type LoadPageModuleFromDocumentOptions = {
27
+ /**
28
+ * Explicit page module URL to import instead of extracting one from the
29
+ * document's hydration assets.
30
+ *
31
+ * React Router uses this during HMR-driven reloads so the active hot module
32
+ * entry wins over any static bootstrap asset references embedded in the HTML.
33
+ */
34
+ moduleUrlOverride?: string;
35
+ };
11
36
  export type InterceptDecision = {
12
37
  shouldIntercept: true;
13
38
  } | {
14
39
  shouldIntercept: false;
15
- reason: 'modified-click' | 'non-left-click' | 'external-target' | 'explicit-reload' | 'download' | 'invalid-href' | 'cross-origin';
40
+ reason: 'modified-click' | 'non-left-click' | 'external-target' | 'explicit-reload' | 'download' | 'invalid-href' | 'cross-origin' | 'same-page-hash';
16
41
  };
42
+ export declare function isSamePageHashNavigationHref(href: string): boolean;
17
43
  /**
18
44
  * Determines whether a link click should be intercepted for client-side navigation.
19
45
  *
@@ -26,7 +52,7 @@ export type InterceptDecision = {
26
52
  */
27
53
  export declare function getInterceptDecision(event: MouseEvent, link: HTMLAnchorElement, options: Required<EcoRouterOptions>): InterceptDecision;
28
54
  /**
29
- * Extracts serialized page props from window.__ECO_PAGE__ or fetched document.
55
+ * Extracts serialized page props from window.__ECO_PAGES__.page or fetched document.
30
56
  * For current document, returns props set by hydration script.
31
57
  * For fetched documents, parses the JSON script tag directly.
32
58
  */
@@ -34,7 +60,7 @@ export declare function extractProps(doc: Document): Record<string, any>;
34
60
  /**
35
61
  * Extracts component module URL using multi-tier strategy.
36
62
  *
37
- * 1. Read from window.__ECO_PAGE__.module (for current document)
63
+ * 1. Read from window.__ECO_PAGES__.page.module (for current document)
38
64
  * 2. Parse inline hydration script with regex (for fetched documents)
39
65
  * 3. Fetch and parse external hydration script (final fallback)
40
66
  *
@@ -52,14 +78,26 @@ export declare function extractComponentUrl(doc: Document): Promise<string | nul
52
78
  * @param url - The URL to load
53
79
  * @returns Object with Component, props, doc, and finalPath, or null on error
54
80
  */
55
- export declare function loadPageModule(url: string): Promise<{
56
- Component: ComponentType<any>;
57
- props: Record<string, any>;
58
- doc: Document;
59
- finalPath: string;
60
- } | null>;
81
+ export declare function loadPageModule(url: string, options?: LoadPageModuleOptions): Promise<LoadedPageModule | null>;
82
+ export declare function fetchPageDocument(url: string, options?: LoadPageModuleOptions): Promise<FetchedPageDocument | null>;
83
+ /**
84
+ * Loads the page module for a fetched or current document.
85
+ *
86
+ * The router normally extracts the page module URL from the document's
87
+ * hydration assets. Callers can provide `options.moduleUrlOverride` when the
88
+ * document is stale with respect to the active runtime module identity, such as
89
+ * during HMR-driven current-page reloads.
90
+ *
91
+ * @param doc - Parsed destination document.
92
+ * @param finalPath - Final route path after redirects.
93
+ * @param options - Module loading overrides.
94
+ * @returns Loaded page module payload or `null` when the document is not a
95
+ * React-router page or no page component can be resolved.
96
+ */
97
+ export declare function loadPageModuleFromDocument(doc: Document, finalPath: string, options?: LoadPageModuleFromDocumentOptions): Promise<LoadedPageModule | null>;
61
98
  /**
62
99
  * Convenience wrapper around getInterceptDecision that returns a boolean.
63
100
  * Use getInterceptDecision directly when you need the reason for debugging.
64
101
  */
65
102
  export declare function shouldInterceptClick(event: MouseEvent, link: HTMLAnchorElement, options: Required<EcoRouterOptions>): boolean;
103
+ export {};