@ecopages/react-router 0.2.0-alpha.5 → 0.2.0-alpha.51
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 +45 -119
- package/package.json +5 -4
- package/src/adapter.js +1 -2
- package/src/head-morpher.d.ts +6 -2
- package/src/head-morpher.js +125 -10
- package/src/hydration-assets.d.ts +12 -0
- package/src/hydration-assets.js +17 -0
- package/src/navigation.d.ts +47 -9
- package/src/navigation.js +94 -35
- package/src/props-script.d.ts +1 -1
- package/src/router.d.ts +5 -1
- package/src/router.js +373 -90
- package/src/scroll-persist.js +15 -7
- package/CHANGELOG.md +0 -12
- package/browser.ts +0 -17
- package/src/adapter.ts +0 -48
- package/src/context.ts +0 -25
- package/src/head-morpher.ts +0 -170
- package/src/index.ts +0 -21
- package/src/manage-scroll.ts +0 -47
- package/src/navigation.ts +0 -247
- package/src/props-script.ts +0 -19
- package/src/router.ts +0 -348
- 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/src/context.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Router context and hook for accessing navigation state.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createContext, useContext } from 'react';
|
|
7
|
-
|
|
8
|
-
export type RouterContextValue = {
|
|
9
|
-
navigate: (url: string) => void;
|
|
10
|
-
isNavigating: boolean;
|
|
11
|
-
};
|
|
12
|
-
|
|
13
|
-
export const RouterContext = createContext<RouterContextValue | null>(null);
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Hook to access the router's navigate function and navigation state.
|
|
17
|
-
* Must be used within an EcoRouter.
|
|
18
|
-
*
|
|
19
|
-
* @throws Error if used outside of EcoRouter
|
|
20
|
-
*/
|
|
21
|
-
export const useRouter = (): RouterContextValue => {
|
|
22
|
-
const context = useContext(RouterContext);
|
|
23
|
-
if (!context) throw new Error('useRouter must be used within EcoRouter');
|
|
24
|
-
return context;
|
|
25
|
-
};
|
package/src/head-morpher.ts
DELETED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Head morphing utilities for client-side navigation.
|
|
3
|
-
* Intelligently syncs head elements between pages using key-based diffing.
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
const PRESERVE_SELECTORS = ['script[type="importmap"]', 'meta[charset]', '[data-eco-persist]'];
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Computes a unique key for a head element to enable diffing.
|
|
11
|
-
* Elements with the same key are considered the same across navigations.
|
|
12
|
-
*/
|
|
13
|
-
function getHeadElementKey(el: Element): string | null {
|
|
14
|
-
const tag = el.tagName.toLowerCase();
|
|
15
|
-
|
|
16
|
-
switch (tag) {
|
|
17
|
-
case 'title':
|
|
18
|
-
return 'title';
|
|
19
|
-
|
|
20
|
-
case 'meta': {
|
|
21
|
-
const name = el.getAttribute('name') || el.getAttribute('property') || el.getAttribute('http-equiv');
|
|
22
|
-
return name ? `meta:${name}` : null;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
case 'link': {
|
|
26
|
-
const rel = el.getAttribute('rel');
|
|
27
|
-
const href = el.getAttribute('href');
|
|
28
|
-
if (rel === 'stylesheet' && href) return `stylesheet:${href}`;
|
|
29
|
-
if (rel === 'icon' || rel === 'shortcut icon') return 'favicon';
|
|
30
|
-
if (rel === 'canonical') return 'canonical';
|
|
31
|
-
return href ? `link:${href}` : null;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
case 'script': {
|
|
35
|
-
if (el.getAttribute('type') === 'importmap') return 'importmap';
|
|
36
|
-
const src = (el as HTMLScriptElement).src;
|
|
37
|
-
return src ? `script:${src}` : null;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
case 'style': {
|
|
41
|
-
const dataId = el.getAttribute('data-eco-style');
|
|
42
|
-
return dataId ? `style:${dataId}` : null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
default:
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Morphs the current document head to match the new document's head.
|
|
52
|
-
* Now splits the process into adding new elements and returning a cleanup function
|
|
53
|
-
* to remove old ones. This is crucial for View Transitions to ensure styles
|
|
54
|
-
* don't disappear before the "old" snapshot is taken.
|
|
55
|
-
*
|
|
56
|
-
* @param newDocument - The parsed document from the navigation target
|
|
57
|
-
* @returns Promise that resolves to a cleanup function when new stylesheets have loaded
|
|
58
|
-
*/
|
|
59
|
-
export async function morphHead(newDocument: Document): Promise<() => void> {
|
|
60
|
-
const currentHead = document.head;
|
|
61
|
-
const newHead = newDocument.head;
|
|
62
|
-
|
|
63
|
-
const currentElements = new Map<string, Element>();
|
|
64
|
-
const newElements = new Map<string, Element>();
|
|
65
|
-
const stylesheetPromises: Promise<void>[] = [];
|
|
66
|
-
const elementsToRemove: Element[] = [];
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* First, map existing head elements by their keys
|
|
70
|
-
* to enable efficient diffing.
|
|
71
|
-
*/
|
|
72
|
-
for (const el of Array.from(currentHead.children)) {
|
|
73
|
-
const key = getHeadElementKey(el);
|
|
74
|
-
if (key) currentElements.set(key, el);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Next, map new head elements by their keys.
|
|
79
|
-
* This allows us to see which elements are new, updated, or removed.
|
|
80
|
-
*/
|
|
81
|
-
for (const el of Array.from(newHead.children)) {
|
|
82
|
-
const key = getHeadElementKey(el);
|
|
83
|
-
if (key) newElements.set(key, el);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Now, iterate over new elements to add or update them in the current head.
|
|
88
|
-
*/
|
|
89
|
-
for (const [key, newEl] of newElements) {
|
|
90
|
-
const currentEl = currentElements.get(key);
|
|
91
|
-
|
|
92
|
-
if (!currentEl) {
|
|
93
|
-
const src = newEl.getAttribute('src');
|
|
94
|
-
/**
|
|
95
|
-
* Skip hydration scripts during SPA navigation to prevent re-mounting
|
|
96
|
-
*
|
|
97
|
-
* In an SPA transition, the EcoRouter is already running and handling the page update.
|
|
98
|
-
* The new page's HTML includes a hydration script (for initial load support), but
|
|
99
|
-
* if we let it execute now, it would re-bootstrap the React app from scratch,
|
|
100
|
-
* causing a full re-mount, state loss, and a visual flash.
|
|
101
|
-
*
|
|
102
|
-
* By blocking this script, we ensure the router maintains control and state.
|
|
103
|
-
*/
|
|
104
|
-
if (newEl.tagName === 'SCRIPT' && src && src.includes('hydration.js') && src.includes('ecopages-react')) {
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const cloned = newEl.cloneNode(true) as Element;
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* If the new element is a stylesheet, we need to wait for it to load
|
|
112
|
-
* before considering the head morph complete. This prevents FOUC.
|
|
113
|
-
*/
|
|
114
|
-
if (cloned.tagName === 'LINK' && (cloned as HTMLLinkElement).rel === 'stylesheet') {
|
|
115
|
-
const loadPromise = new Promise<void>((resolve) => {
|
|
116
|
-
(cloned as HTMLLinkElement).onload = () => resolve();
|
|
117
|
-
(cloned as HTMLLinkElement).onerror = () => resolve();
|
|
118
|
-
});
|
|
119
|
-
stylesheetPromises.push(loadPromise);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
currentHead.appendChild(cloned);
|
|
123
|
-
} else if (key === 'title' && currentEl.textContent !== newEl.textContent) {
|
|
124
|
-
currentEl.textContent = newEl.textContent;
|
|
125
|
-
} else if (key.startsWith('style:') && currentEl.textContent !== newEl.textContent) {
|
|
126
|
-
currentEl.textContent = newEl.textContent;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Finally, handle any new elements without keys (e.g., inline scripts/styles)
|
|
132
|
-
*/
|
|
133
|
-
for (const newEl of Array.from(newHead.children)) {
|
|
134
|
-
const key = getHeadElementKey(newEl);
|
|
135
|
-
if (!key) {
|
|
136
|
-
currentHead.appendChild(newEl.cloneNode(true));
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Wait for all new stylesheets to load before proceeding.
|
|
142
|
-
*/
|
|
143
|
-
if (stylesheetPromises.length > 0) {
|
|
144
|
-
await Promise.all(stylesheetPromises);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Identify and prepare to remove any old elements
|
|
149
|
-
* that are no longer present in the new head.
|
|
150
|
-
*/
|
|
151
|
-
for (const [key, el] of currentElements) {
|
|
152
|
-
if (!newElements.has(key)) {
|
|
153
|
-
const shouldPreserve = PRESERVE_SELECTORS.some((sel) => el.matches(sel));
|
|
154
|
-
if (!shouldPreserve) {
|
|
155
|
-
elementsToRemove.push(el);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Return a cleanup function to remove old elements.
|
|
162
|
-
* This allows the caller to control when the removal happens,
|
|
163
|
-
* which is important for View Transitions.
|
|
164
|
-
*/
|
|
165
|
-
return () => {
|
|
166
|
-
for (const el of elementsToRemove) {
|
|
167
|
-
el.remove();
|
|
168
|
-
}
|
|
169
|
-
};
|
|
170
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* EcoPages React Router - SPA navigation for React with SSR support.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export { EcoRouter, PageContent } from './router.ts';
|
|
7
|
-
export type { EcoRouterProps } from './router.ts';
|
|
8
|
-
|
|
9
|
-
export { EcoPropsScript } from './props-script.ts';
|
|
10
|
-
export type { EcoPropsScriptProps } from './props-script.ts';
|
|
11
|
-
|
|
12
|
-
export { useRouter } from './context.ts';
|
|
13
|
-
export type { RouterContextValue } from './context.ts';
|
|
14
|
-
|
|
15
|
-
export type { EcoRouterOptions } from './types.ts';
|
|
16
|
-
|
|
17
|
-
export { morphHead } from './head-morpher.ts';
|
|
18
|
-
|
|
19
|
-
export type { PageState } from './navigation.ts';
|
|
20
|
-
|
|
21
|
-
export { ecoRouter } from './adapter.ts';
|
package/src/manage-scroll.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manages scroll position during navigations
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { EcoRouterOptions } from './types.ts';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Service for handling scroll position during page transitions.
|
|
10
|
-
* Handles window scroll behavior and hash navigation.
|
|
11
|
-
*/
|
|
12
|
-
/**
|
|
13
|
-
* Handle window scroll position based on scrollBehavior option.
|
|
14
|
-
* Hash links always scroll to target regardless of option.
|
|
15
|
-
*/
|
|
16
|
-
export function manageScroll(
|
|
17
|
-
newUrl: URL,
|
|
18
|
-
previousUrl: URL,
|
|
19
|
-
options: {
|
|
20
|
-
scrollBehavior: Required<EcoRouterOptions>['scrollBehavior'];
|
|
21
|
-
smoothScroll: boolean;
|
|
22
|
-
},
|
|
23
|
-
): void {
|
|
24
|
-
const { scrollBehavior, smoothScroll } = options;
|
|
25
|
-
|
|
26
|
-
if (newUrl.hash) {
|
|
27
|
-
const target = document.getElementById(newUrl.hash.slice(1));
|
|
28
|
-
target?.scrollIntoView({ behavior: smoothScroll ? 'smooth' : 'instant' });
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const behavior = smoothScroll ? 'smooth' : 'instant';
|
|
33
|
-
|
|
34
|
-
switch (scrollBehavior) {
|
|
35
|
-
case 'preserve':
|
|
36
|
-
break;
|
|
37
|
-
case 'auto':
|
|
38
|
-
if (newUrl.pathname !== previousUrl.pathname) {
|
|
39
|
-
window.scrollTo({ top: 0, left: 0, behavior });
|
|
40
|
-
}
|
|
41
|
-
break;
|
|
42
|
-
case 'top':
|
|
43
|
-
default:
|
|
44
|
-
window.scrollTo({ top: 0, left: 0, behavior });
|
|
45
|
-
break;
|
|
46
|
-
}
|
|
47
|
-
}
|
package/src/navigation.ts
DELETED
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Navigation utilities for fetching and parsing page content.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/// <reference types="@ecopages/core/declarations" />
|
|
7
|
-
|
|
8
|
-
import { type ComponentType } from 'react';
|
|
9
|
-
import type { EcoRouterOptions } from './types.ts';
|
|
10
|
-
|
|
11
|
-
export type PageState = {
|
|
12
|
-
Component: ComponentType<any>;
|
|
13
|
-
props: Record<string, any>;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export type InterceptDecision =
|
|
17
|
-
| { shouldIntercept: true }
|
|
18
|
-
| {
|
|
19
|
-
shouldIntercept: false;
|
|
20
|
-
reason:
|
|
21
|
-
| 'modified-click'
|
|
22
|
-
| 'non-left-click'
|
|
23
|
-
| 'external-target'
|
|
24
|
-
| 'explicit-reload'
|
|
25
|
-
| 'download'
|
|
26
|
-
| 'invalid-href'
|
|
27
|
-
| 'cross-origin';
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Determines whether a link click should be intercepted for client-side navigation.
|
|
32
|
-
*
|
|
33
|
-
* Standard SPA navigation rules:
|
|
34
|
-
* - Modified clicks (Cmd/Ctrl/Shift/Alt) open in new tab
|
|
35
|
-
* - Non-left clicks use default browser behavior
|
|
36
|
-
* - External targets, downloads, and cross-origin links navigate normally
|
|
37
|
-
*
|
|
38
|
-
* @returns Object indicating whether to intercept and the reason if not
|
|
39
|
-
*/
|
|
40
|
-
export function getInterceptDecision(
|
|
41
|
-
event: MouseEvent,
|
|
42
|
-
link: HTMLAnchorElement,
|
|
43
|
-
options: Required<EcoRouterOptions>,
|
|
44
|
-
): InterceptDecision {
|
|
45
|
-
if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) {
|
|
46
|
-
return { shouldIntercept: false, reason: 'modified-click' };
|
|
47
|
-
}
|
|
48
|
-
if (event.button !== 0) return { shouldIntercept: false, reason: 'non-left-click' };
|
|
49
|
-
|
|
50
|
-
const target = link.getAttribute('target');
|
|
51
|
-
if (target && target !== '_self') return { shouldIntercept: false, reason: 'external-target' };
|
|
52
|
-
|
|
53
|
-
if (link.hasAttribute(options.reloadAttribute)) return { shouldIntercept: false, reason: 'explicit-reload' };
|
|
54
|
-
if (link.hasAttribute('download')) return { shouldIntercept: false, reason: 'download' };
|
|
55
|
-
|
|
56
|
-
const href = link.getAttribute('href');
|
|
57
|
-
if (!href || href.startsWith('#') || href.startsWith('javascript:')) {
|
|
58
|
-
return { shouldIntercept: false, reason: 'invalid-href' };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const url = new URL(href, window.location.origin);
|
|
62
|
-
if (url.origin !== window.location.origin) return { shouldIntercept: false, reason: 'cross-origin' };
|
|
63
|
-
|
|
64
|
-
return { shouldIntercept: true };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Extracts component module URL from window.__ECO_PAGE__.
|
|
69
|
-
* For current document, returns the module path set by hydration script.
|
|
70
|
-
* For fetched documents, parses the hydration script to extract the module path.
|
|
71
|
-
*/
|
|
72
|
-
function extractComponentUrlFromMarker(doc: Document): string | null {
|
|
73
|
-
if (doc === document && window.__ECO_PAGE__?.module) {
|
|
74
|
-
return window.__ECO_PAGE__.module;
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Matches default import: `import Content from './Content'`
|
|
81
|
-
* Used to extract module path from hydration script for fetched documents.
|
|
82
|
-
*/
|
|
83
|
-
const DEFAULT_IMPORT_REGEX = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/;
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Matches namespace import: `import * as Content from './Content'`
|
|
87
|
-
* Used for MDX components. Also handles minified: `import*as Content from'./Content'`
|
|
88
|
-
*/
|
|
89
|
-
const NAMESPACE_IMPORT_REGEX = /import\s*\*\s*as\s*(\w+)\s*from\s*['"]([^'"]+)['"]/;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Extracts import path from hydration script code using regex.
|
|
93
|
-
* Used for fetched documents. Less reliable due to minification.
|
|
94
|
-
*/
|
|
95
|
-
function extractModulePathFromCode(code: string): string | null {
|
|
96
|
-
const defaultMatch = code.match(DEFAULT_IMPORT_REGEX);
|
|
97
|
-
const namespaceMatch = code.match(NAMESPACE_IMPORT_REGEX);
|
|
98
|
-
return (defaultMatch || namespaceMatch)?.[2] ?? null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Extracts serialized page props from window.__ECO_PAGE__ or fetched document.
|
|
103
|
-
* For current document, returns props set by hydration script.
|
|
104
|
-
* For fetched documents, parses the JSON script tag directly.
|
|
105
|
-
*/
|
|
106
|
-
export function extractProps(doc: Document): Record<string, any> {
|
|
107
|
-
if (doc === document && window.__ECO_PAGE__?.props) {
|
|
108
|
-
return window.__ECO_PAGE__.props;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const propsScript = doc.getElementById('__ECO_PAGE_DATA__');
|
|
112
|
-
if (propsScript?.textContent) {
|
|
113
|
-
try {
|
|
114
|
-
return JSON.parse(propsScript.textContent);
|
|
115
|
-
} catch (e) {
|
|
116
|
-
console.error('[EcoRouter] Failed to parse props:', e);
|
|
117
|
-
return {};
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return {};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Adds cache-busting timestamp for HMR in development.
|
|
126
|
-
*
|
|
127
|
-
* Prevents loading stale cached modules when navigating to previously visited pages.
|
|
128
|
-
* Disabled in production where filenames have content hashes.
|
|
129
|
-
*/
|
|
130
|
-
function addCacheBuster(url: string): string {
|
|
131
|
-
if (import.meta.env?.MODE === 'production' || import.meta.env?.PROD) {
|
|
132
|
-
return url;
|
|
133
|
-
}
|
|
134
|
-
const separator = url.includes('?') ? '&' : '?';
|
|
135
|
-
return `${url}${separator}t=${Date.now()}`;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Extracts component module URL using multi-tier strategy.
|
|
140
|
-
*
|
|
141
|
-
* 1. Read from window.__ECO_PAGE__.module (for current document)
|
|
142
|
-
* 2. Parse inline hydration script with regex (for fetched documents)
|
|
143
|
-
* 3. Fetch and parse external hydration script (final fallback)
|
|
144
|
-
*
|
|
145
|
-
* Regex parsing is less reliable due to minification.
|
|
146
|
-
*/
|
|
147
|
-
export async function extractComponentUrl(doc: Document): Promise<string | null> {
|
|
148
|
-
const markerUrl = extractComponentUrlFromMarker(doc);
|
|
149
|
-
if (markerUrl) return markerUrl;
|
|
150
|
-
|
|
151
|
-
const scripts = Array.from(doc.querySelectorAll('script'));
|
|
152
|
-
|
|
153
|
-
const inlineHydrationScript = scripts.find(
|
|
154
|
-
(s) =>
|
|
155
|
-
!s.src &&
|
|
156
|
-
!!s.textContent &&
|
|
157
|
-
s.textContent.includes('__ECO_PAGE__') &&
|
|
158
|
-
s.textContent.includes('hydrateRoot') &&
|
|
159
|
-
s.textContent.includes('import'),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
if (inlineHydrationScript?.textContent) {
|
|
163
|
-
return extractModulePathFromCode(inlineHydrationScript.textContent);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const hydrationScript = scripts.find((s) => s.src?.includes('hydration.js') && s.src?.includes('ecopages-react'));
|
|
167
|
-
if (!hydrationScript?.src) return null;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const scriptUrl = addCacheBuster(hydrationScript.src);
|
|
171
|
-
const res = await fetch(scriptUrl);
|
|
172
|
-
const code = await res.text();
|
|
173
|
-
return extractModulePathFromCode(code);
|
|
174
|
-
} catch {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Fetches and parses a page, returning its component, props, and document.
|
|
181
|
-
*
|
|
182
|
-
* Flow: Fetch HTML → Parse → Extract props → Extract component URL → Import module
|
|
183
|
-
*
|
|
184
|
-
* Handles multiple export patterns (Content, default.Content, default) for different
|
|
185
|
-
* integration setups. Does NOT update DOM - caller applies changes.
|
|
186
|
-
*
|
|
187
|
-
* @param url - The URL to load
|
|
188
|
-
* @returns Object with Component, props, doc, and finalPath, or null on error
|
|
189
|
-
*/
|
|
190
|
-
export async function loadPageModule(
|
|
191
|
-
url: string,
|
|
192
|
-
): Promise<{ Component: ComponentType<any>; props: Record<string, any>; doc: Document; finalPath: string } | null> {
|
|
193
|
-
try {
|
|
194
|
-
const res = await fetch(url);
|
|
195
|
-
const html = await res.text();
|
|
196
|
-
|
|
197
|
-
const finalUrl = new URL(res.url || url, window.location.origin);
|
|
198
|
-
const finalPath = finalUrl.pathname + finalUrl.search;
|
|
199
|
-
|
|
200
|
-
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
201
|
-
|
|
202
|
-
const props = extractProps(doc);
|
|
203
|
-
const componentUrl = await extractComponentUrl(doc);
|
|
204
|
-
|
|
205
|
-
if (!componentUrl) {
|
|
206
|
-
console.error('[EcoRouter] Could not find component URL');
|
|
207
|
-
return null;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const moduleUrl = addCacheBuster(componentUrl);
|
|
211
|
-
const module = await import(/* @vite-ignore */ moduleUrl);
|
|
212
|
-
const rawComponent = module.Content || module.default?.Content || module.default;
|
|
213
|
-
|
|
214
|
-
const config = module.config || rawComponent?.config;
|
|
215
|
-
|
|
216
|
-
if (!rawComponent) {
|
|
217
|
-
console.error('[EcoRouter] No component found in module');
|
|
218
|
-
return null;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (config && !rawComponent.config) {
|
|
222
|
-
rawComponent.config = config;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
window.__ECO_PAGE__ = {
|
|
226
|
-
module: componentUrl,
|
|
227
|
-
props,
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
return { Component: rawComponent, props, doc, finalPath };
|
|
231
|
-
} catch (e) {
|
|
232
|
-
console.error('[EcoRouter] Navigation failed:', e);
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/**
|
|
238
|
-
* Convenience wrapper around getInterceptDecision that returns a boolean.
|
|
239
|
-
* Use getInterceptDecision directly when you need the reason for debugging.
|
|
240
|
-
*/
|
|
241
|
-
export function shouldInterceptClick(
|
|
242
|
-
event: MouseEvent,
|
|
243
|
-
link: HTMLAnchorElement,
|
|
244
|
-
options: Required<EcoRouterOptions>,
|
|
245
|
-
): boolean {
|
|
246
|
-
return getInterceptDecision(event, link, options).shouldIntercept;
|
|
247
|
-
}
|
package/src/props-script.ts
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { createElement, type FC } from 'react';
|
|
2
|
-
|
|
3
|
-
export interface EcoPropsScriptProps {
|
|
4
|
-
/** The page props to serialize for client-side hydration */
|
|
5
|
-
data: Record<string, any>;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Serializes page props as JSON for SPA navigation.
|
|
10
|
-
* The hydration script reads this and sets window.__ECO_PAGE__.
|
|
11
|
-
* Using application/json allows direct parsing without regex.
|
|
12
|
-
*/
|
|
13
|
-
export const EcoPropsScript: FC<EcoPropsScriptProps> = ({ data }) => {
|
|
14
|
-
return createElement('script', {
|
|
15
|
-
id: '__ECO_PAGE_DATA__',
|
|
16
|
-
type: 'application/json',
|
|
17
|
-
dangerouslySetInnerHTML: { __html: JSON.stringify(data || {}) },
|
|
18
|
-
});
|
|
19
|
-
};
|