@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/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.__ECO_PAGE__.
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.__ECO_PAGE__?.module) {
74
- return window.__ECO_PAGE__.module;
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.__ECO_PAGE__ or fetched document.
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.__ECO_PAGE__?.props) {
108
- return window.__ECO_PAGE__.props;
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('__ECO_PAGE_DATA__');
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.__ECO_PAGE__.module (for current document)
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('__ECO_PAGE__') &&
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?.includes('hydration.js') && s.src?.includes('ecopages-react'));
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
- ): Promise<{ Component: ComponentType<any>; props: Record<string, any>; doc: Document; finalPath: string } | null> {
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
- const props = extractProps(doc);
203
- const componentUrl = await extractComponentUrl(doc);
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
- 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;
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
- 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;
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
- window.__ECO_PAGE__ = {
226
- module: componentUrl,
227
- props,
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
- return { Component: rawComponent, props, doc, finalPath };
231
- } catch (e) {
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
  /**
@@ -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.__ECO_PAGE__.
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>;
@@ -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.__ECO_PAGE__.
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