@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/README.md +1 -1
- package/package.json +5 -5
- package/src/adapter.js +1 -2
- package/src/head-morpher.d.ts +6 -2
- package/src/head-morpher.js +88 -9
- package/src/hydration-assets.d.ts +12 -0
- package/src/hydration-assets.js +17 -0
- package/src/navigation.d.ts +27 -2
- package/src/navigation.js +40 -7
- package/src/router.d.ts +3 -1
- package/src/router.js +249 -183
- package/src/scroll-persist.js +15 -7
- package/CHANGELOG.md +0 -19
- package/browser.ts +0 -17
- package/src/adapter.ts +0 -48
- package/src/context.ts +0 -25
- package/src/head-morpher.ts +0 -214
- package/src/index.ts +0 -21
- package/src/manage-scroll.ts +0 -47
- package/src/navigation.ts +0 -297
- package/src/props-script.ts +0 -19
- package/src/router.ts +0 -670
- package/src/scroll-persist.ts +0 -96
- package/src/types.ts +0 -64
- package/src/view-transition-manager.ts +0 -30
- package/src/view-transition-utils.ts +0 -95
package/README.md
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/react-router",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-beta.0",
|
|
4
4
|
"description": "Client-side SPA router for EcoPages React applications",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -32,12 +32,12 @@
|
|
|
32
32
|
"directory": "packages/react-router"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@ecopages/core": "0.2.
|
|
36
|
-
"@ecopages/react": "0.2.
|
|
35
|
+
"@ecopages/core": "0.2.1-beta.0",
|
|
36
|
+
"@ecopages/react": "0.2.1-beta.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"react": "^19",
|
|
40
|
-
"react-dom": "^19.2.
|
|
39
|
+
"react": "^19.2.6",
|
|
40
|
+
"react-dom": "^19.2.6"
|
|
41
41
|
},
|
|
42
42
|
"types": "./src/index.d.ts"
|
|
43
43
|
}
|
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
|
|
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"
|
package/src/head-morpher.d.ts
CHANGED
|
@@ -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
|
|
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<
|
|
19
|
+
export declare function morphHead(newDocument: Document): Promise<HeadMorphResult>;
|
package/src/head-morpher.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
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;
|
|
2
5
|
function isNonExecutableHeadScript(el) {
|
|
3
6
|
if (el.tagName !== "SCRIPT") {
|
|
4
7
|
return false;
|
|
@@ -31,6 +34,17 @@ function shouldPersistExecutableInlineHeadScript(el) {
|
|
|
31
34
|
}
|
|
32
35
|
return !isNonExecutableHeadScript(el);
|
|
33
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
|
+
}
|
|
34
48
|
function getHeadElementKey(el) {
|
|
35
49
|
const tag = el.tagName.toLowerCase();
|
|
36
50
|
switch (tag) {
|
|
@@ -49,10 +63,9 @@ function getHeadElementKey(el) {
|
|
|
49
63
|
return href ? `link:${href}` : null;
|
|
50
64
|
}
|
|
51
65
|
case "script": {
|
|
52
|
-
if (el.getAttribute("type") === "importmap") return "importmap";
|
|
53
66
|
const scriptId = el.getAttribute("data-eco-script-id") || el.getAttribute("id");
|
|
54
67
|
if (scriptId) return `script-id:${scriptId}`;
|
|
55
|
-
const src = el.src;
|
|
68
|
+
const src = el.getAttribute(RERUN_SRC_ATTR) || el.src;
|
|
56
69
|
return src ? `script:${src}` : null;
|
|
57
70
|
}
|
|
58
71
|
case "style": {
|
|
@@ -70,6 +83,12 @@ async function morphHead(newDocument) {
|
|
|
70
83
|
const newElements = /* @__PURE__ */ new Map();
|
|
71
84
|
const stylesheetPromises = [];
|
|
72
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
|
+
}));
|
|
73
92
|
for (const el of Array.from(currentHead.children)) {
|
|
74
93
|
const key = getHeadElementKey(el);
|
|
75
94
|
if (key) currentElements.set(key, el);
|
|
@@ -80,9 +99,11 @@ async function morphHead(newDocument) {
|
|
|
80
99
|
}
|
|
81
100
|
for (const [key, newEl] of newElements) {
|
|
82
101
|
const currentEl = currentElements.get(key);
|
|
102
|
+
if (isRerunScript(newEl)) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
83
105
|
if (!currentEl) {
|
|
84
|
-
|
|
85
|
-
if (newEl.tagName === "SCRIPT" && src && src.includes("hydration.js") && src.includes("ecopages-react")) {
|
|
106
|
+
if (newEl.tagName === "SCRIPT" && isHydrationScript(newEl)) {
|
|
86
107
|
continue;
|
|
87
108
|
}
|
|
88
109
|
const cloned = newEl.cloneNode(true);
|
|
@@ -104,7 +125,7 @@ async function morphHead(newDocument) {
|
|
|
104
125
|
}
|
|
105
126
|
for (const newEl of Array.from(newHead.children)) {
|
|
106
127
|
const key = getHeadElementKey(newEl);
|
|
107
|
-
if (!key) {
|
|
128
|
+
if (!key && !isRerunScript(newEl)) {
|
|
108
129
|
currentHead.appendChild(newEl.cloneNode(true));
|
|
109
130
|
}
|
|
110
131
|
}
|
|
@@ -119,12 +140,70 @@ async function morphHead(newDocument) {
|
|
|
119
140
|
}
|
|
120
141
|
}
|
|
121
142
|
}
|
|
122
|
-
return
|
|
123
|
-
|
|
124
|
-
el
|
|
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 registeredRerun = getRegisteredRerunScript(script.scriptId);
|
|
152
|
+
const replacement = document.createElement("script");
|
|
153
|
+
const shouldBustModuleSrc = isExternalModuleRerunScript(script) && !registeredRerun;
|
|
154
|
+
for (const [name, value] of script.attributes) {
|
|
155
|
+
if (name === "src" && shouldBustModuleSrc) {
|
|
156
|
+
replacement.setAttribute(RERUN_SRC_ATTR, value);
|
|
157
|
+
replacement.setAttribute("src", createRerunScriptUrl(value));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
replacement.setAttribute(name, value);
|
|
161
|
+
}
|
|
162
|
+
replacement.textContent = script.textContent;
|
|
163
|
+
const existingScript = findExistingRerunScript(script);
|
|
164
|
+
if (registeredRerun) {
|
|
165
|
+
if (!existingScript) {
|
|
166
|
+
document.head.appendChild(replacement);
|
|
167
|
+
}
|
|
168
|
+
registeredRerun();
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (existingScript) {
|
|
172
|
+
existingScript.replaceWith(replacement);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
document.head.appendChild(replacement);
|
|
176
|
+
}
|
|
125
177
|
}
|
|
126
178
|
};
|
|
127
179
|
}
|
|
180
|
+
function findExistingRerunScript(script) {
|
|
181
|
+
const scripts = Array.from(document.head.querySelectorAll("script"));
|
|
182
|
+
if (script.scriptId) {
|
|
183
|
+
return scripts.find((candidate) => candidate.getAttribute("data-eco-script-id") === script.scriptId) ?? null;
|
|
184
|
+
}
|
|
185
|
+
return scripts.find(
|
|
186
|
+
(candidate) => (candidate.getAttribute(RERUN_SRC_ATTR) ?? candidate.getAttribute("src")) === script.src && (candidate.textContent ?? "") === script.textContent
|
|
187
|
+
) ?? null;
|
|
188
|
+
}
|
|
189
|
+
function isExternalModuleRerunScript(script) {
|
|
190
|
+
if (!script.src) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
return script.attributes.some(([name, value]) => name === "type" && value === "module");
|
|
194
|
+
}
|
|
195
|
+
function getRegisteredRerunScript(scriptId) {
|
|
196
|
+
if (!scriptId) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const runtimeWindow = window;
|
|
200
|
+
return runtimeWindow.__ECO_PAGES__?.rerunScripts?.[scriptId] ?? null;
|
|
201
|
+
}
|
|
202
|
+
function createRerunScriptUrl(src) {
|
|
203
|
+
const url = new URL(src, document.baseURI);
|
|
204
|
+
url.searchParams.set("__eco_rerun", String(++rerunNonce));
|
|
205
|
+
return url.toString();
|
|
206
|
+
}
|
|
128
207
|
export {
|
|
129
208
|
morphHead
|
|
130
209
|
};
|
|
@@ -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
|
+
};
|
package/src/navigation.d.ts
CHANGED
|
@@ -23,12 +23,23 @@ export type FetchedPageDocument = {
|
|
|
23
23
|
type LoadPageModuleOptions = {
|
|
24
24
|
signal?: AbortSignal;
|
|
25
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
|
+
};
|
|
26
36
|
export type InterceptDecision = {
|
|
27
37
|
shouldIntercept: true;
|
|
28
38
|
} | {
|
|
29
39
|
shouldIntercept: false;
|
|
30
|
-
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';
|
|
31
41
|
};
|
|
42
|
+
export declare function isSamePageHashNavigationHref(href: string): boolean;
|
|
32
43
|
/**
|
|
33
44
|
* Determines whether a link click should be intercepted for client-side navigation.
|
|
34
45
|
*
|
|
@@ -69,7 +80,21 @@ export declare function extractComponentUrl(doc: Document): Promise<string | nul
|
|
|
69
80
|
*/
|
|
70
81
|
export declare function loadPageModule(url: string, options?: LoadPageModuleOptions): Promise<LoadedPageModule | null>;
|
|
71
82
|
export declare function fetchPageDocument(url: string, options?: LoadPageModuleOptions): Promise<FetchedPageDocument | null>;
|
|
72
|
-
|
|
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>;
|
|
73
98
|
/**
|
|
74
99
|
* Convenience wrapper around getInterceptDecision that returns a boolean.
|
|
75
100
|
* Use getInterceptDecision directly when you need the reason for debugging.
|
package/src/navigation.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { getEcoDocumentOwner } from "@ecopages/core/router/navigation-coordinator";
|
|
2
|
+
import { isReactPageHydrationAssetSrc } from "./hydration-assets.js";
|
|
2
3
|
const ROUTER_PROPS_SCRIPT_ID = "__ECO_PAGE_DATA__";
|
|
3
|
-
function
|
|
4
|
-
|
|
4
|
+
function isSamePageHashNavigationHref(href) {
|
|
5
|
+
if (!href) {
|
|
6
|
+
return false;
|
|
7
|
+
}
|
|
8
|
+
const currentUrl = new URL(window.location.href);
|
|
9
|
+
const targetUrl = new URL(href, currentUrl);
|
|
10
|
+
return targetUrl.origin === currentUrl.origin && targetUrl.hash.length > 0 && targetUrl.pathname === currentUrl.pathname && targetUrl.search === currentUrl.search;
|
|
5
11
|
}
|
|
6
12
|
function getInterceptDecision(event, link, options) {
|
|
7
13
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
@@ -18,6 +24,9 @@ function getInterceptDecision(event, link, options) {
|
|
|
18
24
|
}
|
|
19
25
|
const url = new URL(href, window.location.origin);
|
|
20
26
|
if (url.origin !== window.location.origin) return { shouldIntercept: false, reason: "cross-origin" };
|
|
27
|
+
if (isSamePageHashNavigationHref(href)) {
|
|
28
|
+
return { shouldIntercept: false, reason: "same-page-hash" };
|
|
29
|
+
}
|
|
21
30
|
return { shouldIntercept: true };
|
|
22
31
|
}
|
|
23
32
|
function extractComponentUrlFromMarker(doc) {
|
|
@@ -28,7 +37,30 @@ function extractComponentUrlFromMarker(doc) {
|
|
|
28
37
|
}
|
|
29
38
|
const DEFAULT_IMPORT_REGEX = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/;
|
|
30
39
|
const NAMESPACE_IMPORT_REGEX = /import\s*\*\s*as\s*(\w+)\s*from\s*['"]([^'"]+)['"]/;
|
|
31
|
-
|
|
40
|
+
const PAGE_MODULE_MARKER_REGEX = /__ECO_PAGES__\.page\s*=\s*\{\s*module\s*:\s*['"]([^'"]+)['"]/;
|
|
41
|
+
const PAGE_MODULE_IDENTIFIER_REGEX = /__ECO_PAGES__\.page\s*=\s*\{\s*module\s*:\s*([A-Za-z_$][\w$]*)\s*,/;
|
|
42
|
+
const PAGE_BOOTSTRAP_SELECTOR = 'script[data-eco-page-bootstrap="react-router"]';
|
|
43
|
+
function extractModulePathFromCode(code, fallbackUrl) {
|
|
44
|
+
const markerMatch = code.match(PAGE_MODULE_MARKER_REGEX);
|
|
45
|
+
if (markerMatch) {
|
|
46
|
+
return markerMatch[1] ?? null;
|
|
47
|
+
}
|
|
48
|
+
const moduleIdentifier = code.match(PAGE_MODULE_IDENTIFIER_REGEX)?.[1];
|
|
49
|
+
if (moduleIdentifier) {
|
|
50
|
+
const assignmentRegex = new RegExp(
|
|
51
|
+
`(?:const|let|var)[^;]*\\b${moduleIdentifier}\\s*=\\s*(?:['"]([^'"]+)['"]|(import\\.meta\\.url))`
|
|
52
|
+
);
|
|
53
|
+
const assignmentMatch = code.match(assignmentRegex);
|
|
54
|
+
if (assignmentMatch?.[1]) {
|
|
55
|
+
return assignmentMatch[1];
|
|
56
|
+
}
|
|
57
|
+
if (fallbackUrl && assignmentMatch?.[2]) {
|
|
58
|
+
return fallbackUrl;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (fallbackUrl && code.includes("module:import.meta.url")) {
|
|
62
|
+
return fallbackUrl;
|
|
63
|
+
}
|
|
32
64
|
const defaultMatch = code.match(DEFAULT_IMPORT_REGEX);
|
|
33
65
|
const namespaceMatch = code.match(NAMESPACE_IMPORT_REGEX);
|
|
34
66
|
return (defaultMatch || namespaceMatch)?.[2] ?? null;
|
|
@@ -68,13 +100,13 @@ async function extractComponentUrl(doc) {
|
|
|
68
100
|
if (inlineHydrationScript?.textContent) {
|
|
69
101
|
return extractModulePathFromCode(inlineHydrationScript.textContent);
|
|
70
102
|
}
|
|
71
|
-
const hydrationScript = scripts.find((s) =>
|
|
103
|
+
const hydrationScript = doc.querySelector(PAGE_BOOTSTRAP_SELECTOR) ?? scripts.find((s) => isReactPageHydrationAssetSrc(s.src ?? ""));
|
|
72
104
|
if (!hydrationScript?.src) return null;
|
|
73
105
|
try {
|
|
74
106
|
const scriptUrl = addCacheBuster(hydrationScript.src);
|
|
75
107
|
const res = await fetch(scriptUrl);
|
|
76
108
|
const code = await res.text();
|
|
77
|
-
return extractModulePathFromCode(code);
|
|
109
|
+
return extractModulePathFromCode(code, hydrationScript.src);
|
|
78
110
|
} catch {
|
|
79
111
|
return null;
|
|
80
112
|
}
|
|
@@ -107,9 +139,9 @@ async function fetchPageDocument(url, options = {}) {
|
|
|
107
139
|
return null;
|
|
108
140
|
}
|
|
109
141
|
}
|
|
110
|
-
async function loadPageModuleFromDocument(doc, finalPath) {
|
|
142
|
+
async function loadPageModuleFromDocument(doc, finalPath, options = {}) {
|
|
111
143
|
const props = extractProps(doc);
|
|
112
|
-
const componentUrl = await extractComponentUrl(doc);
|
|
144
|
+
const componentUrl = options.moduleUrlOverride ?? await extractComponentUrl(doc);
|
|
113
145
|
if (!componentUrl) {
|
|
114
146
|
if (isReactRouteDocument(doc)) {
|
|
115
147
|
console.error("[EcoRouter] Could not find component URL");
|
|
@@ -140,6 +172,7 @@ export {
|
|
|
140
172
|
extractProps,
|
|
141
173
|
fetchPageDocument,
|
|
142
174
|
getInterceptDecision,
|
|
175
|
+
isSamePageHashNavigationHref,
|
|
143
176
|
loadPageModule,
|
|
144
177
|
loadPageModuleFromDocument,
|
|
145
178
|
shouldInterceptClick
|
package/src/router.d.ts
CHANGED
|
@@ -31,7 +31,9 @@ export declare function clearLayoutCache(): void;
|
|
|
31
31
|
* Must be a child of {@link EcoRouter}. When `persistLayouts` is enabled,
|
|
32
32
|
* shared layouts remain mounted across navigations. When the server serialized
|
|
33
33
|
* request `locals` for hydration, the same `locals` object is passed to the
|
|
34
|
-
* layout on the client so the hydrated tree matches SSR.
|
|
34
|
+
* layout on the client so the hydrated tree matches SSR. Refresh-style updates
|
|
35
|
+
* may still replace the cached layout implementation when the router marks the
|
|
36
|
+
* page state with `refreshPersistedLayout`.
|
|
35
37
|
*
|
|
36
38
|
* @example
|
|
37
39
|
* ```tsx
|