@ecopages/react-router 0.2.0-alpha.4 → 0.2.0-alpha.7
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 +9 -2
- package/README.md +46 -120
- package/package.json +3 -2
- package/src/head-morpher.js +37 -1
- package/src/head-morpher.ts +45 -1
- package/src/navigation.d.ts +21 -8
- package/src/navigation.js +59 -33
- package/src/navigation.ts +86 -36
- package/src/props-script.d.ts +1 -1
- package/src/props-script.ts +1 -1
- package/src/router.d.ts +3 -1
- package/src/router.js +295 -59
- package/src/router.ts +392 -70
package/src/navigation.ts
CHANGED
|
@@ -5,14 +5,39 @@
|
|
|
5
5
|
|
|
6
6
|
/// <reference types="@ecopages/core/declarations" />
|
|
7
7
|
|
|
8
|
+
import { getEcoDocumentOwner } from '@ecopages/core/router/navigation-coordinator';
|
|
8
9
|
import { type ComponentType } from 'react';
|
|
9
10
|
import type { EcoRouterOptions } from './types.ts';
|
|
10
11
|
|
|
12
|
+
const ROUTER_PROPS_SCRIPT_ID = '__ECO_PAGE_DATA__';
|
|
13
|
+
|
|
14
|
+
function isReactPageHydrationAsset(src: string): boolean {
|
|
15
|
+
return src.includes('ecopages-react-') && src.includes('hydration.js') && !src.includes('ecopages-react-island-');
|
|
16
|
+
}
|
|
17
|
+
|
|
11
18
|
export type PageState = {
|
|
12
19
|
Component: ComponentType<any>;
|
|
13
20
|
props: Record<string, any>;
|
|
14
21
|
};
|
|
15
22
|
|
|
23
|
+
export type LoadedPageModule = {
|
|
24
|
+
Component: ComponentType<any>;
|
|
25
|
+
props: Record<string, any>;
|
|
26
|
+
doc: Document;
|
|
27
|
+
finalPath: string;
|
|
28
|
+
moduleUrl: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FetchedPageDocument = {
|
|
32
|
+
doc: Document;
|
|
33
|
+
finalPath: string;
|
|
34
|
+
html: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type LoadPageModuleOptions = {
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
};
|
|
40
|
+
|
|
16
41
|
export type InterceptDecision =
|
|
17
42
|
| { shouldIntercept: true }
|
|
18
43
|
| {
|
|
@@ -65,13 +90,13 @@ export function getInterceptDecision(
|
|
|
65
90
|
}
|
|
66
91
|
|
|
67
92
|
/**
|
|
68
|
-
* Extracts component module URL from window.
|
|
93
|
+
* Extracts component module URL from window.__ECO_PAGES__.page.
|
|
69
94
|
* For current document, returns the module path set by hydration script.
|
|
70
95
|
* For fetched documents, parses the hydration script to extract the module path.
|
|
71
96
|
*/
|
|
72
97
|
function extractComponentUrlFromMarker(doc: Document): string | null {
|
|
73
|
-
if (doc === document && window.
|
|
74
|
-
return window.
|
|
98
|
+
if (doc === document && window.__ECO_PAGES__?.page?.module) {
|
|
99
|
+
return window.__ECO_PAGES__.page.module;
|
|
75
100
|
}
|
|
76
101
|
return null;
|
|
77
102
|
}
|
|
@@ -99,16 +124,16 @@ function extractModulePathFromCode(code: string): string | null {
|
|
|
99
124
|
}
|
|
100
125
|
|
|
101
126
|
/**
|
|
102
|
-
* Extracts serialized page props from window.
|
|
127
|
+
* Extracts serialized page props from window.__ECO_PAGES__.page or fetched document.
|
|
103
128
|
* For current document, returns props set by hydration script.
|
|
104
129
|
* For fetched documents, parses the JSON script tag directly.
|
|
105
130
|
*/
|
|
106
131
|
export function extractProps(doc: Document): Record<string, any> {
|
|
107
|
-
if (doc === document && window.
|
|
108
|
-
return window.
|
|
132
|
+
if (doc === document && window.__ECO_PAGES__?.page?.props) {
|
|
133
|
+
return window.__ECO_PAGES__.page.props;
|
|
109
134
|
}
|
|
110
135
|
|
|
111
|
-
const propsScript = doc.getElementById(
|
|
136
|
+
const propsScript = doc.getElementById(ROUTER_PROPS_SCRIPT_ID);
|
|
112
137
|
if (propsScript?.textContent) {
|
|
113
138
|
try {
|
|
114
139
|
return JSON.parse(propsScript.textContent);
|
|
@@ -121,6 +146,10 @@ export function extractProps(doc: Document): Record<string, any> {
|
|
|
121
146
|
return {};
|
|
122
147
|
}
|
|
123
148
|
|
|
149
|
+
function isReactRouteDocument(doc: Document): boolean {
|
|
150
|
+
return getEcoDocumentOwner(doc) === 'react-router';
|
|
151
|
+
}
|
|
152
|
+
|
|
124
153
|
/**
|
|
125
154
|
* Adds cache-busting timestamp for HMR in development.
|
|
126
155
|
*
|
|
@@ -138,7 +167,7 @@ function addCacheBuster(url: string): string {
|
|
|
138
167
|
/**
|
|
139
168
|
* Extracts component module URL using multi-tier strategy.
|
|
140
169
|
*
|
|
141
|
-
* 1. Read from window.
|
|
170
|
+
* 1. Read from window.__ECO_PAGES__.page.module (for current document)
|
|
142
171
|
* 2. Parse inline hydration script with regex (for fetched documents)
|
|
143
172
|
* 3. Fetch and parse external hydration script (final fallback)
|
|
144
173
|
*
|
|
@@ -154,7 +183,7 @@ export async function extractComponentUrl(doc: Document): Promise<string | null>
|
|
|
154
183
|
(s) =>
|
|
155
184
|
!s.src &&
|
|
156
185
|
!!s.textContent &&
|
|
157
|
-
s.textContent.includes('
|
|
186
|
+
s.textContent.includes('__ECO_PAGES__') &&
|
|
158
187
|
s.textContent.includes('hydrateRoot') &&
|
|
159
188
|
s.textContent.includes('import'),
|
|
160
189
|
);
|
|
@@ -163,7 +192,7 @@ export async function extractComponentUrl(doc: Document): Promise<string | null>
|
|
|
163
192
|
return extractModulePathFromCode(inlineHydrationScript.textContent);
|
|
164
193
|
}
|
|
165
194
|
|
|
166
|
-
const hydrationScript = scripts.find((s) => s.src
|
|
195
|
+
const hydrationScript = scripts.find((s) => isReactPageHydrationAsset(s.src ?? ''));
|
|
167
196
|
if (!hydrationScript?.src) return null;
|
|
168
197
|
|
|
169
198
|
try {
|
|
@@ -189,9 +218,27 @@ export async function extractComponentUrl(doc: Document): Promise<string | null>
|
|
|
189
218
|
*/
|
|
190
219
|
export async function loadPageModule(
|
|
191
220
|
url: string,
|
|
192
|
-
|
|
221
|
+
options: LoadPageModuleOptions = {},
|
|
222
|
+
): Promise<LoadedPageModule | null> {
|
|
223
|
+
const fetchedPage = await fetchPageDocument(url, options);
|
|
224
|
+
if (!fetchedPage) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return loadPageModuleFromDocument(fetchedPage.doc, fetchedPage.finalPath);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function fetchPageDocument(
|
|
232
|
+
url: string,
|
|
233
|
+
options: LoadPageModuleOptions = {},
|
|
234
|
+
): Promise<FetchedPageDocument | null> {
|
|
193
235
|
try {
|
|
194
|
-
const res = await fetch(url
|
|
236
|
+
const res = await fetch(url, {
|
|
237
|
+
signal: options.signal,
|
|
238
|
+
headers: {
|
|
239
|
+
Accept: 'text/html',
|
|
240
|
+
},
|
|
241
|
+
});
|
|
195
242
|
const html = await res.text();
|
|
196
243
|
|
|
197
244
|
const finalUrl = new URL(res.url || url, window.location.origin);
|
|
@@ -199,39 +246,42 @@ export async function loadPageModule(
|
|
|
199
246
|
|
|
200
247
|
const doc = new DOMParser().parseFromString(html, 'text/html');
|
|
201
248
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
if (!componentUrl) {
|
|
206
|
-
console.error('[EcoRouter] Could not find component URL');
|
|
249
|
+
return { doc, finalPath, html };
|
|
250
|
+
} catch (e) {
|
|
251
|
+
if (e instanceof DOMException && e.name === 'AbortError') {
|
|
207
252
|
return null;
|
|
208
253
|
}
|
|
254
|
+
console.error('[EcoRouter] Navigation failed:', e);
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
209
258
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const config = module.config || rawComponent?.config;
|
|
259
|
+
export async function loadPageModuleFromDocument(doc: Document, finalPath: string): Promise<LoadedPageModule | null> {
|
|
260
|
+
const props = extractProps(doc);
|
|
261
|
+
const componentUrl = await extractComponentUrl(doc);
|
|
215
262
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
if (config && !rawComponent.config) {
|
|
222
|
-
rawComponent.config = config;
|
|
263
|
+
if (!componentUrl) {
|
|
264
|
+
if (isReactRouteDocument(doc)) {
|
|
265
|
+
console.error('[EcoRouter] Could not find component URL');
|
|
223
266
|
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
224
269
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
270
|
+
const moduleUrl = addCacheBuster(componentUrl);
|
|
271
|
+
const module = await import(/* @vite-ignore */ moduleUrl);
|
|
272
|
+
const rawComponent = module.Content || module.default?.Content || module.default;
|
|
273
|
+
const config = module.config || rawComponent?.config;
|
|
229
274
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
console.error('[EcoRouter] Navigation failed:', e);
|
|
275
|
+
if (!rawComponent) {
|
|
276
|
+
console.error('[EcoRouter] No component found in module');
|
|
233
277
|
return null;
|
|
234
278
|
}
|
|
279
|
+
|
|
280
|
+
if (config && !rawComponent.config) {
|
|
281
|
+
rawComponent.config = config;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { Component: rawComponent, props, doc, finalPath, moduleUrl: componentUrl };
|
|
235
285
|
}
|
|
236
286
|
|
|
237
287
|
/**
|
package/src/props-script.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export interface EcoPropsScriptProps {
|
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
7
|
* Serializes page props as JSON for SPA navigation.
|
|
8
|
-
* The hydration script reads this and sets window.
|
|
8
|
+
* The hydration script reads this and sets window.__ECO_PAGES__.page.
|
|
9
9
|
* Using application/json allows direct parsing without regex.
|
|
10
10
|
*/
|
|
11
11
|
export declare const EcoPropsScript: FC<EcoPropsScriptProps>;
|
package/src/props-script.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface EcoPropsScriptProps {
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Serializes page props as JSON for SPA navigation.
|
|
10
|
-
* The hydration script reads this and sets window.
|
|
10
|
+
* The hydration script reads this and sets window.__ECO_PAGES__.page.
|
|
11
11
|
* Using application/json allows direct parsing without regex.
|
|
12
12
|
*/
|
|
13
13
|
export const EcoPropsScript: FC<EcoPropsScriptProps> = ({ data }) => {
|
package/src/router.d.ts
CHANGED
|
@@ -29,7 +29,9 @@ export declare function clearLayoutCache(): void;
|
|
|
29
29
|
* Renders the current page with its layout.
|
|
30
30
|
*
|
|
31
31
|
* Must be a child of {@link EcoRouter}. When `persistLayouts` is enabled,
|
|
32
|
-
* shared layouts remain mounted across navigations.
|
|
32
|
+
* shared layouts remain mounted across navigations. When the server serialized
|
|
33
|
+
* request `locals` for hydration, the same `locals` object is passed to the
|
|
34
|
+
* layout on the client so the hydrated tree matches SSR.
|
|
33
35
|
*
|
|
34
36
|
* @example
|
|
35
37
|
* ```tsx
|