@ecopages/react 0.2.0-alpha.25 → 0.2.0-alpha.27
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 +7 -4
- package/README.md +17 -16
- package/package.json +3 -3
- package/src/react-hmr-strategy.d.ts +17 -12
- package/src/react-hmr-strategy.js +16 -21
- package/src/react-renderer.d.ts +26 -24
- package/src/react-renderer.js +57 -49
- package/src/react.plugin.d.ts +2 -4
- package/src/react.plugin.js +11 -11
- package/src/router-adapter.d.ts +7 -14
- package/src/services/react-bundle.service.d.ts +1 -1
- package/src/services/react-bundle.service.js +10 -10
- package/src/services/react-hydration-asset.service.d.ts +3 -3
- package/src/services/react-hydration-asset.service.js +14 -7
- package/src/services/react-page-payload.service.js +1 -1
- package/src/services/react-runtime-bundle.service.d.ts +2 -2
- package/src/services/react-runtime-bundle.service.js +5 -5
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/hydration-scripts.d.ts +2 -0
- package/src/utils/hydration-scripts.js +10 -6
- package/src/utils/hydration-scripts.test.browser.js +0 -1
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +1 -1
- package/src/utils/{react-runtime-specifier-map.js → react-runtime-alias-map.js} +4 -8
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,11 +10,11 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
10
10
|
|
|
11
11
|
- Fixed router-managed React HMR page entries to reload the active route with a cleared persisted-layout cache so shared layout edits apply while the current page stays mounted.
|
|
12
12
|
- Fixed router-managed React HMR handlers to forward the active page HMR entry when reloading the current route through React Router.
|
|
13
|
-
- Fixed React route hydration bundles to
|
|
13
|
+
- Fixed production React route hydration bundles to inline React runtime dependencies and import the router through the emitted page browser graph instead of a published import-map key.
|
|
14
14
|
- Removed the redundant React page props bootstrap script so route hydration relies on the canonical `__ECO_PAGE_DATA__` payload.
|
|
15
|
-
- Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer
|
|
15
|
+
- Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer foreign-subtree resolution across Bun, Vite, and Nitro flows.
|
|
16
16
|
- Restored direct `ReactPlugin` construction so the exported class still accepts the public plugin options shape.
|
|
17
|
-
- Fixed React
|
|
17
|
+
- Fixed React foreign-subtree payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
|
|
18
18
|
|
|
19
19
|
### Features
|
|
20
20
|
|
|
@@ -23,6 +23,9 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
23
23
|
### Refactoring
|
|
24
24
|
|
|
25
25
|
- Collapsed React route hydration into one page-owned entry module that re-exports the page component and bundles runtime dependencies in production.
|
|
26
|
+
- Removed the router adapter `importMapKey` contract so both development and production route hydration follow the router bundle import path instead of split import-map and bundle-path models.
|
|
27
|
+
- Replaced the positional `ReactHmrStrategy` constructor with an options object so React HMR wiring can evolve without argument-order churn.
|
|
28
|
+
- Renamed the remaining React runtime alias internals away from `specifierMap` terminology now that import-map-era core seams are gone.
|
|
26
29
|
- Consolidated React bundling, hydration, and runtime state behind shared service boundaries and `window.__ECO_PAGES__`.
|
|
27
30
|
- Moved React plugin option/default resolution into the factory and replaced renderer static config with instance-owned runtime wiring.
|
|
28
31
|
- Extracted React page-payload and locals serialization into a dedicated service to keep the renderer focused on orchestration.
|
|
@@ -34,7 +37,7 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
34
37
|
|
|
35
38
|
- Added Vitest browser coverage for the React `dynamic()` utility using React Testing Library.
|
|
36
39
|
- Added browser execution coverage for the generated React hydration bootstrap, including router ownership registration and page-root cleanup.
|
|
37
|
-
- Added renderer-level coverage for the
|
|
40
|
+
- Added renderer-level coverage for the foreign-subtree payload compatibility contract, including non-attachable fragment roots.
|
|
38
41
|
|
|
39
42
|
### Documentation
|
|
40
43
|
|
package/README.md
CHANGED
|
@@ -5,7 +5,8 @@ First-class integration for [React 19](https://react.dev/) in Ecopages. This plu
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun add @ecopages/react
|
|
8
|
+
bun add @ecopages/react react react-dom
|
|
9
|
+
bun add -d @types/react @types/react-dom
|
|
9
10
|
```
|
|
10
11
|
|
|
11
12
|
## Usage
|
|
@@ -26,15 +27,15 @@ export default config;
|
|
|
26
27
|
|
|
27
28
|
## Component-Level Islands
|
|
28
29
|
|
|
29
|
-
|
|
30
|
+
For component-level islands, Ecopages React uses this contract:
|
|
30
31
|
|
|
31
32
|
- SSR output preserves the authored DOM structure (no unnecessary wrapper elements).
|
|
32
33
|
- A stable `data-eco-component-id` attribute is attached to the component SSR root.
|
|
33
|
-
- The client
|
|
34
|
+
- The island runtime replaces the SSR host with a dedicated client-owned container and mounts it with `createRoot()`. Full-page hydration paths use `hydrateRoot()`.
|
|
34
35
|
|
|
35
36
|
> [!TIP]
|
|
36
37
|
> **Full React SPA Routing:**
|
|
37
|
-
> If you are building full React pages and want client-side navigation (SPA), use [@ecopages/react-router](
|
|
38
|
+
> If you are building full React pages and want client-side navigation (SPA), use [@ecopages/react-router](../../react-router/README.md) and pass it to the react plugin: `reactPlugin({ router: ecoRouter() })`.
|
|
38
39
|
|
|
39
40
|
## MDX Support
|
|
40
41
|
|
|
@@ -65,15 +66,15 @@ export default config;
|
|
|
65
66
|
The React integration can participate in mixed-renderer apps in three ways:
|
|
66
67
|
|
|
67
68
|
- React can own the page or view directly.
|
|
68
|
-
- React can render nested
|
|
69
|
+
- React can render nested foreign subtrees inside pages owned by another integration.
|
|
69
70
|
- React can render through non-React page, layout, or document shells when those shell components return strings.
|
|
70
71
|
|
|
71
|
-
When a non-React render pass
|
|
72
|
+
When a non-React render pass reaches a React-owned foreign child, Ecopages hands that foreign subtree back to the React renderer. When React renders through a non-React shell, that shell must serialize to HTML so React can insert the result into the final response without escaping it.
|
|
72
73
|
|
|
73
74
|
Important:
|
|
74
75
|
|
|
75
76
|
- Components that may render foreign children must declare those children in `config.dependencies.components`.
|
|
76
|
-
- Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not infer every foreign
|
|
77
|
+
- Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not infer every foreign subtree from rendered HTML alone.
|
|
77
78
|
- React still keeps its own child transport and hydration rules for React-owned subtrees.
|
|
78
79
|
|
|
79
80
|
## Server and Client Graph Contract
|
|
@@ -115,7 +116,7 @@ The client bundle keeps:
|
|
|
115
116
|
- The page component render path.
|
|
116
117
|
- Client-safe component dependencies reachable from render.
|
|
117
118
|
- Layout wiring needed for hydration.
|
|
118
|
-
- Router runtime state needed by [@ecopages/react-router](
|
|
119
|
+
- Router runtime state needed by [@ecopages/react-router](../../react-router/README.md) when SPA mode is enabled.
|
|
119
120
|
|
|
120
121
|
The client bundle removes or excludes:
|
|
121
122
|
|
|
@@ -130,7 +131,7 @@ Important:
|
|
|
130
131
|
|
|
131
132
|
### AST Pipeline Order
|
|
132
133
|
|
|
133
|
-
The browser-bound transform in [
|
|
134
|
+
The browser-bound transform in [src/utils/client-graph-boundary-plugin.ts](src/utils/client-graph-boundary-plugin.ts) follows this order:
|
|
134
135
|
|
|
135
136
|
1. Parse the module and build a reachability view of the client render graph.
|
|
136
137
|
2. Remove imports that are not allowed or not reachable from the client graph.
|
|
@@ -165,7 +166,7 @@ The fix is to strip server-only `eco.page(...)` options after import pruning, wh
|
|
|
165
166
|
|
|
166
167
|
The browser must not receive arbitrary request-scoped data.
|
|
167
168
|
|
|
168
|
-
The React renderer in [
|
|
169
|
+
The React renderer in [src/react-renderer.ts](src/react-renderer.ts) serializes only the top-level `locals` keys explicitly declared by `Page.requires`. If a page does not declare `requires`, no `locals` are serialized for hydration.
|
|
169
170
|
|
|
170
171
|
Example:
|
|
171
172
|
|
|
@@ -190,8 +191,8 @@ Hydration must rebuild the same tree the server rendered.
|
|
|
190
191
|
|
|
191
192
|
That applies to both:
|
|
192
193
|
|
|
193
|
-
- non-router hydration scripts in [
|
|
194
|
-
- router-backed hydration in [
|
|
194
|
+
- non-router hydration scripts in [src/utils/hydration-scripts.ts](src/utils/hydration-scripts.ts)
|
|
195
|
+
- router-backed hydration in [../../react-router/src/router.ts](../../react-router/src/router.ts)
|
|
195
196
|
|
|
196
197
|
If the page render receives `locals` on the server and the layout also depends on those values, the client must pass the same serialized `locals` into the layout during hydration. Otherwise React will detect a mismatch.
|
|
197
198
|
|
|
@@ -199,9 +200,9 @@ If the page render receives `locals` on the server and the layout also depends o
|
|
|
199
200
|
|
|
200
201
|
The main regression coverage lives in:
|
|
201
202
|
|
|
202
|
-
- [
|
|
203
|
-
- [
|
|
204
|
-
- [
|
|
205
|
-
- [
|
|
203
|
+
- [src/utils/client-graph-boundary-plugin.test.ts](src/utils/client-graph-boundary-plugin.test.ts): verifies server-only `eco.page(...)` options are stripped from browser bundles.
|
|
204
|
+
- [src/react-renderer.locals.test.ts](src/react-renderer.locals.test.ts): verifies only declared `requires` keys are serialized into hydration payloads.
|
|
205
|
+
- [src/utils/hydration-scripts.test.ts](src/utils/hydration-scripts.test.ts): verifies non-router hydration passes serialized `locals` into layouts.
|
|
206
|
+
- [../../react-router/test/hmr-reload.test.browser.ts](../../react-router/test/hmr-reload.test.browser.ts): verifies router-backed layout hydration receives `locals` with `persistLayouts` both enabled and disabled.
|
|
206
207
|
|
|
207
208
|
If you change the AST transform or hydration flow, update the corresponding tests in the same change.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ecopages/react",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.27",
|
|
4
4
|
"description": "React integration for Ecopages",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ecopages",
|
|
@@ -53,14 +53,14 @@
|
|
|
53
53
|
"directory": "packages/integrations/react"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
|
-
"@ecopages/core": "0.2.0-alpha.
|
|
56
|
+
"@ecopages/core": "0.2.0-alpha.27",
|
|
57
57
|
"@types/react": "^19",
|
|
58
58
|
"@types/react-dom": "^19",
|
|
59
59
|
"react": "^19",
|
|
60
60
|
"react-dom": "^19"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@ecopages/file-system": "0.2.0-alpha.
|
|
63
|
+
"@ecopages/file-system": "0.2.0-alpha.27",
|
|
64
64
|
"@ecopages/logger": "^0.2.3",
|
|
65
65
|
"@mdx-js/esbuild": "^3.0.1",
|
|
66
66
|
"@mdx-js/mdx": "^3.1.0",
|
|
@@ -10,6 +10,15 @@ import { HmrStrategy, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
|
|
|
10
10
|
import type { DefaultHmrContext } from '@ecopages/core';
|
|
11
11
|
import type { CompileOptions } from '@mdx-js/mdx';
|
|
12
12
|
import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
|
|
13
|
+
export interface ReactHmrStrategyOptions {
|
|
14
|
+
context: DefaultHmrContext;
|
|
15
|
+
pageMetadataCache: ReactHmrPageMetadataCache;
|
|
16
|
+
runtimeAliasMap: ReadonlyMap<string, string>;
|
|
17
|
+
mdxCompilerOptions?: CompileOptions;
|
|
18
|
+
ownedTemplateExtensions?: string[];
|
|
19
|
+
allTemplateExtensions?: string[];
|
|
20
|
+
explicitGraphEnabled?: boolean;
|
|
21
|
+
}
|
|
13
22
|
/**
|
|
14
23
|
* Strategy for handling React component HMR updates.
|
|
15
24
|
*
|
|
@@ -39,13 +48,16 @@ import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metada
|
|
|
39
48
|
* ```typescript
|
|
40
49
|
* const context = {
|
|
41
50
|
* getWatchedFiles: () => watchedFilesMap,
|
|
42
|
-
* getSpecifierMap: () => specifierMap,
|
|
43
51
|
* getDistDir: () => '/path/to/dist/_hmr',
|
|
44
52
|
* getPlugins: () => [],
|
|
45
53
|
* getSrcDir: () => '/path/to/src',
|
|
46
54
|
* getLayoutsDir: () => '/path/to/src/layouts'
|
|
47
55
|
* };
|
|
48
|
-
* const strategy = new ReactHmrStrategy(
|
|
56
|
+
* const strategy = new ReactHmrStrategy({
|
|
57
|
+
* context,
|
|
58
|
+
* pageMetadataCache,
|
|
59
|
+
* runtimeAliasMap
|
|
60
|
+
* });
|
|
49
61
|
* ```
|
|
50
62
|
*/
|
|
51
63
|
export declare class ReactHmrStrategy extends HmrStrategy {
|
|
@@ -57,20 +69,13 @@ export declare class ReactHmrStrategy extends HmrStrategy {
|
|
|
57
69
|
/**
|
|
58
70
|
* Creates a new React HMR strategy instance.
|
|
59
71
|
*
|
|
60
|
-
* @param
|
|
61
|
-
* and the layouts directory for detecting layout file changes that require full
|
|
62
|
-
* page reloads instead of module-level HMR updates.
|
|
63
|
-
* @param pageMetadataCache - React-only cache of declared browser modules discovered during
|
|
64
|
-
* server rendering. This avoids re-importing unchanged page modules
|
|
65
|
-
* during save-time Fast Refresh rebuilds.
|
|
66
|
-
* @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
|
|
67
|
-
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
68
|
-
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
72
|
+
* @param options - React HMR runtime services and behavior flags.
|
|
69
73
|
*/
|
|
70
74
|
private context;
|
|
71
75
|
private pageMetadataCache;
|
|
72
76
|
private explicitGraphEnabled;
|
|
73
|
-
|
|
77
|
+
private readonly runtimeAliasMap;
|
|
78
|
+
constructor(options: ReactHmrStrategyOptions);
|
|
74
79
|
/**
|
|
75
80
|
* Returns build plugins for React HMR bundling.
|
|
76
81
|
*
|
|
@@ -7,7 +7,7 @@ import { Logger } from "@ecopages/logger";
|
|
|
7
7
|
import { injectHmrHandler } from "./utils/hmr-scripts.js";
|
|
8
8
|
import { createClientGraphBoundaryPlugin } from "./utils/client-graph-boundary-plugin.js";
|
|
9
9
|
import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from "./utils/declared-modules.js";
|
|
10
|
-
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-
|
|
10
|
+
import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-alias-map.js";
|
|
11
11
|
import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
|
|
12
12
|
const appLogger = new Logger("[ReactHmrStrategy]");
|
|
13
13
|
class ReactHmrStrategy extends HmrStrategy {
|
|
@@ -21,27 +21,23 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
21
21
|
/**
|
|
22
22
|
* Creates a new React HMR strategy instance.
|
|
23
23
|
*
|
|
24
|
-
* @param
|
|
25
|
-
* and the layouts directory for detecting layout file changes that require full
|
|
26
|
-
* page reloads instead of module-level HMR updates.
|
|
27
|
-
* @param pageMetadataCache - React-only cache of declared browser modules discovered during
|
|
28
|
-
* server rendering. This avoids re-importing unchanged page modules
|
|
29
|
-
* during save-time Fast Refresh rebuilds.
|
|
30
|
-
* @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
|
|
31
|
-
* @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
|
|
32
|
-
* In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
|
|
24
|
+
* @param options - React HMR runtime services and behavior flags.
|
|
33
25
|
*/
|
|
34
26
|
context;
|
|
35
27
|
pageMetadataCache;
|
|
36
28
|
explicitGraphEnabled;
|
|
37
|
-
|
|
29
|
+
runtimeAliasMap;
|
|
30
|
+
constructor(options) {
|
|
38
31
|
super();
|
|
39
|
-
this.context = context;
|
|
40
|
-
this.pageMetadataCache = pageMetadataCache;
|
|
41
|
-
this.
|
|
42
|
-
this.
|
|
43
|
-
this.
|
|
44
|
-
this.
|
|
32
|
+
this.context = options.context;
|
|
33
|
+
this.pageMetadataCache = options.pageMetadataCache;
|
|
34
|
+
this.runtimeAliasMap = options.runtimeAliasMap;
|
|
35
|
+
this.explicitGraphEnabled = options.explicitGraphEnabled ?? false;
|
|
36
|
+
this.mdxCompilerOptions = options.mdxCompilerOptions;
|
|
37
|
+
this.ownedTemplateExtensions = new Set(options.ownedTemplateExtensions ?? [".tsx"]);
|
|
38
|
+
this.allTemplateExtensions = [...options.allTemplateExtensions ?? [".tsx"]].sort(
|
|
39
|
+
(a, b) => b.length - a.length
|
|
40
|
+
);
|
|
45
41
|
}
|
|
46
42
|
/**
|
|
47
43
|
* Returns build plugins for React HMR bundling.
|
|
@@ -50,9 +46,8 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
50
46
|
* (including `node:*`) from breaking the browser bundle.
|
|
51
47
|
*/
|
|
52
48
|
getBuildPlugins(declaredModules) {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, {
|
|
49
|
+
const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.runtimeAliasMap.keys());
|
|
50
|
+
const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.runtimeAliasMap, {
|
|
56
51
|
name: "react-hmr-runtime-specifier-alias"
|
|
57
52
|
});
|
|
58
53
|
return [
|
|
@@ -287,7 +282,7 @@ class ReactHmrStrategy extends HmrStrategy {
|
|
|
287
282
|
}
|
|
288
283
|
try {
|
|
289
284
|
let code = await fileSystem.readFile(tempPath);
|
|
290
|
-
code = rewriteRuntimeSpecifierAliases(code, this.
|
|
285
|
+
code = rewriteRuntimeSpecifierAliases(code, this.runtimeAliasMap);
|
|
291
286
|
code = injectHmrHandler(code);
|
|
292
287
|
await fileSystem.writeAsync(finalPath, code);
|
|
293
288
|
await fileSystem.removeAsync(tempPath).catch(() => {
|
package/src/react-renderer.d.ts
CHANGED
|
@@ -115,15 +115,15 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
115
115
|
*/
|
|
116
116
|
private renderNonReactShellComponent;
|
|
117
117
|
/**
|
|
118
|
-
* Renders one React component
|
|
118
|
+
* Renders one React component while preserving already-resolved child HTML.
|
|
119
119
|
*
|
|
120
|
-
* When nested
|
|
121
|
-
*
|
|
120
|
+
* When nested foreign-subtree resolution has already produced child HTML for this
|
|
121
|
+
* component, the child payload must remain raw SSR output rather than a React
|
|
122
122
|
* string child, otherwise React would escape it. This helper renders a unique
|
|
123
123
|
* token through React and swaps that token back to the resolved HTML
|
|
124
124
|
* afterward.
|
|
125
125
|
*
|
|
126
|
-
* @param input Component render input for the current
|
|
126
|
+
* @param input Component render input for the current render step.
|
|
127
127
|
* @param context React-specific render context for stable token generation.
|
|
128
128
|
* @returns Serialized component HTML with resolved child markup preserved.
|
|
129
129
|
*/
|
|
@@ -131,31 +131,31 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
131
131
|
/**
|
|
132
132
|
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
133
133
|
*
|
|
134
|
-
* Queued
|
|
134
|
+
* Queued foreign-subtree resolution may render children through a fragment path before all
|
|
135
135
|
* nested integration tokens are resolved. When that happens, React must never see
|
|
136
136
|
* the resolved child HTML as a normal string child or it would escape it. The
|
|
137
137
|
* runtime context stores the placeholder token and the raw child HTML so the
|
|
138
|
-
* fragment render path can reinsert it before foreign
|
|
138
|
+
* fragment render path can reinsert it before foreign-subtree tokens are handled.
|
|
139
139
|
*/
|
|
140
140
|
private restoreRuntimeChildHtml;
|
|
141
141
|
/**
|
|
142
|
-
* Renders queued child content through React and then resolves nested
|
|
142
|
+
* Renders queued child content through React and then resolves nested foreign-subtree tokens.
|
|
143
143
|
*
|
|
144
144
|
* This path is only used for children that were deferred while React rendered the
|
|
145
|
-
* parent
|
|
146
|
-
* current runtime context, then asks the shared queued-
|
|
145
|
+
* parent component. It first restores any raw child HTML placeholders owned by the
|
|
146
|
+
* current runtime context, then asks the shared queued foreign-subtree resolver to swap
|
|
147
147
|
* foreign integration tokens with their resolved HTML.
|
|
148
148
|
*/
|
|
149
149
|
private renderQueuedChildrenToHtml;
|
|
150
150
|
/**
|
|
151
|
-
* Resolves queued renderer-owned
|
|
151
|
+
* Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
|
|
152
152
|
*
|
|
153
|
-
* React components can enqueue nested
|
|
153
|
+
* React components can enqueue nested foreign subtrees while the parent HTML is being
|
|
154
154
|
* rendered. This delegates to the shared renderer-owned queue resolver but keeps
|
|
155
155
|
* the React-specific child rendering behavior local so raw child HTML and React's
|
|
156
156
|
* fragment rendering semantics stay coordinated.
|
|
157
157
|
*/
|
|
158
|
-
private
|
|
158
|
+
private resolveQueuedForeignSubtreeHtml;
|
|
159
159
|
private buildHydrationProps;
|
|
160
160
|
/**
|
|
161
161
|
* Builds the extra document props needed when React renders through a non-React HTML shell.
|
|
@@ -165,21 +165,21 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
165
165
|
*/
|
|
166
166
|
private buildNonReactDocumentProps;
|
|
167
167
|
/**
|
|
168
|
-
* Renders a foreign integration component
|
|
168
|
+
* Renders a foreign integration component that participates in React composition.
|
|
169
169
|
*
|
|
170
170
|
* Non-React components must resolve to serialized HTML so React can embed them as
|
|
171
|
-
* mixed-shell
|
|
172
|
-
* through the shared dependency resolver before queued
|
|
171
|
+
* mixed-shell children. Any component-owned dependencies still need to flow
|
|
172
|
+
* through the shared dependency resolver before queued foreign-subtree tokens are finalized.
|
|
173
173
|
*/
|
|
174
|
-
private
|
|
174
|
+
private renderForeignComponentWithSerializedHtml;
|
|
175
175
|
/**
|
|
176
|
-
* Renders a React-owned component
|
|
176
|
+
* Renders a React-owned component and attaches island hydration metadata when possible.
|
|
177
177
|
*
|
|
178
|
-
* This path keeps React-owned SSR, queued
|
|
178
|
+
* This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
|
|
179
179
|
* island hydration wiring together so the public `renderComponent()` method can
|
|
180
180
|
* read as orchestration rather than implementation detail.
|
|
181
181
|
*/
|
|
182
|
-
private
|
|
182
|
+
private renderReactManagedComponent;
|
|
183
183
|
/**
|
|
184
184
|
* Renders a React component for component-level orchestration.
|
|
185
185
|
*
|
|
@@ -188,17 +188,17 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
188
188
|
* - When an explicit component instance id is provided, a stable
|
|
189
189
|
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
190
190
|
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
191
|
-
* - When resolved child HTML is provided, that
|
|
191
|
+
* - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
|
|
192
192
|
* composition step and does not emit hydration assets for the parent wrapper.
|
|
193
193
|
*
|
|
194
194
|
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
195
195
|
* deterministic mount target per component instance.
|
|
196
196
|
*/
|
|
197
197
|
renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
|
|
198
|
-
protected
|
|
199
|
-
|
|
198
|
+
protected createForeignChildRuntime(options: {
|
|
199
|
+
renderInput: ComponentRenderInput;
|
|
200
200
|
rendererCache: Map<string, IntegrationRenderer<any>>;
|
|
201
|
-
}): import("@ecopages/core").
|
|
201
|
+
}): import("@ecopages/core").ForeignChildRuntime;
|
|
202
202
|
/**
|
|
203
203
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
204
204
|
* @param filePath - The file path to check
|
|
@@ -208,7 +208,9 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
208
208
|
protected usesIntegrationPageImporter(file: string): boolean;
|
|
209
209
|
protected importIntegrationPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
|
|
210
210
|
protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
|
|
211
|
-
|
|
211
|
+
buildPageBrowserGraph(pagePath: string): Promise<{
|
|
212
|
+
assets: ProcessedAsset[];
|
|
213
|
+
}>;
|
|
212
214
|
/**
|
|
213
215
|
* Renders a full route response for the filesystem page pipeline.
|
|
214
216
|
*
|
package/src/react-renderer.js
CHANGED
|
@@ -178,8 +178,8 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
178
178
|
if (!filePath) {
|
|
179
179
|
return;
|
|
180
180
|
}
|
|
181
|
-
const
|
|
182
|
-
this.appendProcessedDependencies(
|
|
181
|
+
const pageBrowserGraph = await this.buildPageBrowserGraph(filePath);
|
|
182
|
+
this.appendProcessedDependencies(pageBrowserGraph.assets);
|
|
183
183
|
}
|
|
184
184
|
/**
|
|
185
185
|
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
@@ -196,22 +196,22 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
196
196
|
throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
|
|
197
197
|
}
|
|
198
198
|
/**
|
|
199
|
-
* Renders one React component
|
|
199
|
+
* Renders one React component while preserving already-resolved child HTML.
|
|
200
200
|
*
|
|
201
|
-
* When nested
|
|
202
|
-
*
|
|
201
|
+
* When nested foreign-subtree resolution has already produced child HTML for this
|
|
202
|
+
* component, the child payload must remain raw SSR output rather than a React
|
|
203
203
|
* string child, otherwise React would escape it. This helper renders a unique
|
|
204
204
|
* token through React and swaps that token back to the resolved HTML
|
|
205
205
|
* afterward.
|
|
206
206
|
*
|
|
207
|
-
* @param input Component render input for the current
|
|
207
|
+
* @param input Component render input for the current render step.
|
|
208
208
|
* @param context React-specific render context for stable token generation.
|
|
209
209
|
* @returns Serialized component HTML with resolved child markup preserved.
|
|
210
210
|
*/
|
|
211
211
|
renderComponentHtml(input, context, runtimeContext) {
|
|
212
212
|
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
213
213
|
if (input.children === void 0) {
|
|
214
|
-
return this.
|
|
214
|
+
return this.normalizeUnresolvedMarkerArtifactHtml(
|
|
215
215
|
reactDomServer.renderToString(react.createElement(this.asReactComponent(input.component), input.props))
|
|
216
216
|
);
|
|
217
217
|
}
|
|
@@ -224,16 +224,16 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
224
224
|
const html = reactDomServer.renderToString(
|
|
225
225
|
react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
226
226
|
);
|
|
227
|
-
return this.
|
|
227
|
+
return this.normalizeUnresolvedMarkerArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
228
228
|
}
|
|
229
229
|
/**
|
|
230
230
|
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
231
231
|
*
|
|
232
|
-
* Queued
|
|
232
|
+
* Queued foreign-subtree resolution may render children through a fragment path before all
|
|
233
233
|
* nested integration tokens are resolved. When that happens, React must never see
|
|
234
234
|
* the resolved child HTML as a normal string child or it would escape it. The
|
|
235
235
|
* runtime context stores the placeholder token and the raw child HTML so the
|
|
236
|
-
* fragment render path can reinsert it before foreign
|
|
236
|
+
* fragment render path can reinsert it before foreign-subtree tokens are handled.
|
|
237
237
|
*/
|
|
238
238
|
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
239
239
|
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
@@ -242,11 +242,11 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
242
242
|
return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
|
|
243
243
|
}
|
|
244
244
|
/**
|
|
245
|
-
* Renders queued child content through React and then resolves nested
|
|
245
|
+
* Renders queued child content through React and then resolves nested foreign-subtree tokens.
|
|
246
246
|
*
|
|
247
247
|
* This path is only used for children that were deferred while React rendered the
|
|
248
|
-
* parent
|
|
249
|
-
* current runtime context, then asks the shared queued-
|
|
248
|
+
* parent component. It first restores any raw child HTML placeholders owned by the
|
|
249
|
+
* current runtime context, then asks the shared queued foreign-subtree resolver to swap
|
|
250
250
|
* foreign integration tokens with their resolved HTML.
|
|
251
251
|
*/
|
|
252
252
|
async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
|
|
@@ -254,26 +254,34 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
254
254
|
return void 0;
|
|
255
255
|
}
|
|
256
256
|
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
257
|
-
let html = this.
|
|
257
|
+
let html = this.normalizeUnresolvedMarkerArtifactHtml(
|
|
258
258
|
reactDomServer.renderToString(react.createElement(react.Fragment, null, children))
|
|
259
259
|
);
|
|
260
260
|
html = this.restoreRuntimeChildHtml(html, runtimeContext);
|
|
261
|
-
html = await this.
|
|
261
|
+
html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(
|
|
262
|
+
html,
|
|
263
|
+
queuedResolutionsByToken,
|
|
264
|
+
resolveToken
|
|
265
|
+
);
|
|
262
266
|
return html;
|
|
263
267
|
}
|
|
264
268
|
/**
|
|
265
|
-
* Resolves queued renderer-owned
|
|
269
|
+
* Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
|
|
266
270
|
*
|
|
267
|
-
* React components can enqueue nested
|
|
271
|
+
* React components can enqueue nested foreign subtrees while the parent HTML is being
|
|
268
272
|
* rendered. This delegates to the shared renderer-owned queue resolver but keeps
|
|
269
273
|
* the React-specific child rendering behavior local so raw child HTML and React's
|
|
270
274
|
* fragment rendering semantics stay coordinated.
|
|
271
275
|
*/
|
|
272
|
-
async
|
|
273
|
-
return this.
|
|
276
|
+
async resolveQueuedForeignSubtreeHtml(html, runtimeContext) {
|
|
277
|
+
return this.foreignSubtreeExecutionService.resolveQueuedHtml({
|
|
278
|
+
currentIntegrationName: this.name,
|
|
274
279
|
html,
|
|
275
280
|
runtimeContext,
|
|
276
281
|
queueLabel: "React",
|
|
282
|
+
getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
|
|
283
|
+
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
284
|
+
dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
|
|
277
285
|
renderQueuedChildren: async (children, currentRuntimeContext, queuedResolutionsByToken, resolveToken) => {
|
|
278
286
|
const renderedHtml = await this.renderQueuedChildrenToHtml(
|
|
279
287
|
children,
|
|
@@ -310,13 +318,13 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
310
318
|
};
|
|
311
319
|
}
|
|
312
320
|
/**
|
|
313
|
-
* Renders a foreign integration component
|
|
321
|
+
* Renders a foreign integration component that participates in React composition.
|
|
314
322
|
*
|
|
315
323
|
* Non-React components must resolve to serialized HTML so React can embed them as
|
|
316
|
-
* mixed-shell
|
|
317
|
-
* through the shared dependency resolver before queued
|
|
324
|
+
* mixed-shell children. Any component-owned dependencies still need to flow
|
|
325
|
+
* through the shared dependency resolver before queued foreign-subtree tokens are finalized.
|
|
318
326
|
*/
|
|
319
|
-
async
|
|
327
|
+
async renderForeignComponentWithSerializedHtml(input, runtimeContext) {
|
|
320
328
|
let props = input.props;
|
|
321
329
|
if (input.children !== void 0) {
|
|
322
330
|
props = {
|
|
@@ -332,35 +340,35 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
332
340
|
const hasDependencies = Boolean(input.component.config?.dependencies);
|
|
333
341
|
const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
|
|
334
342
|
const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
|
|
335
|
-
const
|
|
343
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
|
|
336
344
|
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
337
345
|
...assets ?? [],
|
|
338
|
-
...
|
|
346
|
+
...queuedForeignSubtreeResolution.assets
|
|
339
347
|
]);
|
|
340
348
|
return {
|
|
341
|
-
html:
|
|
349
|
+
html: queuedForeignSubtreeResolution.html,
|
|
342
350
|
canAttachAttributes: true,
|
|
343
|
-
rootTag: this.getRootTagName(
|
|
351
|
+
rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
|
|
344
352
|
integrationName: this.name,
|
|
345
353
|
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
346
354
|
};
|
|
347
355
|
}
|
|
348
356
|
/**
|
|
349
|
-
* Renders a React-owned component
|
|
357
|
+
* Renders a React-owned component and attaches island hydration metadata when possible.
|
|
350
358
|
*
|
|
351
|
-
* This path keeps React-owned SSR, queued
|
|
359
|
+
* This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
|
|
352
360
|
* island hydration wiring together so the public `renderComponent()` method can
|
|
353
361
|
* read as orchestration rather than implementation detail.
|
|
354
362
|
*/
|
|
355
|
-
async
|
|
363
|
+
async renderReactManagedComponent(input, runtimeContext) {
|
|
356
364
|
const componentConfig = input.component.config;
|
|
357
365
|
const context = {
|
|
358
366
|
componentInstanceId: input.integrationContext?.componentInstanceId
|
|
359
367
|
};
|
|
360
368
|
const hasResolvedChildHtml = input.children !== void 0;
|
|
361
369
|
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
362
|
-
const
|
|
363
|
-
html =
|
|
370
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
|
|
371
|
+
html = queuedForeignSubtreeResolution.html;
|
|
364
372
|
const canAttachAttributes = hasSingleRootElement(html);
|
|
365
373
|
const rootTag = this.getRootTagName(html);
|
|
366
374
|
const componentFile = componentConfig?.__eco?.file;
|
|
@@ -377,7 +385,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
377
385
|
}
|
|
378
386
|
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
379
387
|
...assets ?? [],
|
|
380
|
-
...
|
|
388
|
+
...queuedForeignSubtreeResolution.assets
|
|
381
389
|
]);
|
|
382
390
|
return {
|
|
383
391
|
html,
|
|
@@ -396,27 +404,27 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
396
404
|
* - When an explicit component instance id is provided, a stable
|
|
397
405
|
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
398
406
|
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
399
|
-
* - When resolved child HTML is provided, that
|
|
407
|
+
* - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
|
|
400
408
|
* composition step and does not emit hydration assets for the parent wrapper.
|
|
401
409
|
*
|
|
402
410
|
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
403
411
|
* deterministic mount target per component instance.
|
|
404
412
|
*/
|
|
405
413
|
async renderComponent(input) {
|
|
406
|
-
const runtimeContext = this.
|
|
414
|
+
const runtimeContext = this.getQueuedForeignSubtreeResolutionContext(input);
|
|
407
415
|
if (!this.isReactManagedComponent(input.component)) {
|
|
408
|
-
return this.
|
|
416
|
+
return this.renderForeignComponentWithSerializedHtml(input, runtimeContext);
|
|
409
417
|
}
|
|
410
|
-
return this.
|
|
418
|
+
return this.renderReactManagedComponent(input, runtimeContext);
|
|
411
419
|
}
|
|
412
|
-
|
|
413
|
-
return this.
|
|
414
|
-
|
|
420
|
+
createForeignChildRuntime(options) {
|
|
421
|
+
return this.createQueuedForeignSubtreeExecutionRuntime({
|
|
422
|
+
renderInput: options.renderInput,
|
|
415
423
|
rendererCache: options.rendererCache,
|
|
416
424
|
createRuntimeContext: (integrationContext, rendererCache) => ({
|
|
417
425
|
rendererCache,
|
|
418
426
|
componentInstanceScope: integrationContext.componentInstanceId,
|
|
419
|
-
|
|
427
|
+
nextForeignSubtreeId: 0,
|
|
420
428
|
queuedResolutions: [],
|
|
421
429
|
rawChildrenToken: void 0,
|
|
422
430
|
rawChildrenHtml: void 0
|
|
@@ -450,16 +458,16 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
450
458
|
config
|
|
451
459
|
};
|
|
452
460
|
}
|
|
453
|
-
async
|
|
461
|
+
async buildPageBrowserGraph(pagePath) {
|
|
454
462
|
try {
|
|
455
463
|
const pageModule = await this.importPageFile(pagePath);
|
|
456
464
|
const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
|
|
457
465
|
if (!shouldHydrate) {
|
|
458
|
-
return [];
|
|
466
|
+
return { assets: [] };
|
|
459
467
|
}
|
|
460
468
|
const isMdx = this.pageModuleService.isMdxFile(pagePath);
|
|
461
469
|
const declaredModules = this.pageModuleService.collectPageDeclaredModules(pageModule);
|
|
462
|
-
const processedAssets = await this.hydrationAssetService.
|
|
470
|
+
const processedAssets = await this.hydrationAssetService.buildPageBrowserGraphAssets(
|
|
463
471
|
pagePath,
|
|
464
472
|
isMdx,
|
|
465
473
|
declaredModules
|
|
@@ -470,9 +478,9 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
470
478
|
config: pageModule.config,
|
|
471
479
|
processComponentDependencies: async (components) => await this.processComponentDependencies(components)
|
|
472
480
|
});
|
|
473
|
-
return [...processedAssets, ...mdxConfigAssets];
|
|
481
|
+
return { assets: [...processedAssets, ...mdxConfigAssets] };
|
|
474
482
|
}
|
|
475
|
-
return processedAssets;
|
|
483
|
+
return { assets: processedAssets };
|
|
476
484
|
} catch (error) {
|
|
477
485
|
if (error instanceof BundleError) {
|
|
478
486
|
console.error("[ecopages] Bundle errors:", error.logs);
|
|
@@ -560,16 +568,16 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
560
568
|
const metadata = await this.resolveViewMetadata(view, props);
|
|
561
569
|
await this.prepareViewDependencies(view, Layout);
|
|
562
570
|
await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
|
|
563
|
-
const viewRender = await this.
|
|
571
|
+
const viewRender = await this.renderComponentWithForeignChildren({
|
|
564
572
|
component: view,
|
|
565
573
|
props: normalizedProps
|
|
566
574
|
});
|
|
567
|
-
const layoutRender = Layout ? await this.
|
|
575
|
+
const layoutRender = Layout ? await this.renderComponentWithForeignChildren({
|
|
568
576
|
component: Layout,
|
|
569
577
|
props: {},
|
|
570
578
|
children: viewRender.html
|
|
571
579
|
}) : void 0;
|
|
572
|
-
const documentRender = await this.
|
|
580
|
+
const documentRender = await this.renderComponentWithForeignChildren({
|
|
573
581
|
component: HtmlTemplate,
|
|
574
582
|
props: {
|
|
575
583
|
metadata,
|
package/src/react.plugin.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
2
|
-
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
1
|
+
import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
3
2
|
import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
|
|
4
3
|
import type React from 'react';
|
|
5
4
|
import { ReactRenderer } from './react-renderer.js';
|
|
@@ -13,7 +12,7 @@ export declare const PLUGIN_NAME = "react";
|
|
|
13
12
|
* The React plugin class
|
|
14
13
|
* This plugin provides support for React components in Ecopages
|
|
15
14
|
*/
|
|
16
|
-
export declare class ReactPlugin extends IntegrationPlugin<React.
|
|
15
|
+
export declare class ReactPlugin extends IntegrationPlugin<React.ReactNode> {
|
|
17
16
|
renderer: typeof ReactRenderer;
|
|
18
17
|
private readonly routerAdapter;
|
|
19
18
|
private readonly mdxEnabled;
|
|
@@ -66,7 +65,6 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
|
|
|
66
65
|
* @returns ReactHmrStrategy instance for handling React component updates
|
|
67
66
|
*/
|
|
68
67
|
getHmrStrategy(): HmrStrategy | undefined;
|
|
69
|
-
getRuntimeSpecifierMap(): Record<string, string>;
|
|
70
68
|
}
|
|
71
69
|
/**
|
|
72
70
|
* Factory function to create a React plugin instance
|
package/src/react.plugin.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
IntegrationPlugin
|
|
3
|
+
} from "@ecopages/core/plugins/integration-plugin";
|
|
2
4
|
import { Logger } from "@ecopages/logger";
|
|
3
5
|
import { REACT_PLUGIN_NAME } from "./react.constants.js";
|
|
4
6
|
import { ReactRenderer } from "./react-renderer.js";
|
|
@@ -174,17 +176,15 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
174
176
|
return void 0;
|
|
175
177
|
}
|
|
176
178
|
const context = this.hmrManager.getDefaultContext();
|
|
177
|
-
return new ReactHmrStrategy(
|
|
179
|
+
return new ReactHmrStrategy({
|
|
178
180
|
context,
|
|
179
|
-
this.hmrPageMetadataCache,
|
|
180
|
-
this.
|
|
181
|
-
this.
|
|
182
|
-
this.
|
|
183
|
-
this.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
getRuntimeSpecifierMap() {
|
|
187
|
-
return this.runtimeBundleService.getSpecifierMap();
|
|
181
|
+
pageMetadataCache: this.hmrPageMetadataCache,
|
|
182
|
+
runtimeAliasMap: new Map(Object.entries(this.runtimeBundleService.getRuntimeAliasMap("development"))),
|
|
183
|
+
mdxCompilerOptions: this.mdxCompilerOptions,
|
|
184
|
+
ownedTemplateExtensions: this.extensions,
|
|
185
|
+
allTemplateExtensions: this.appConfig.templatesExt,
|
|
186
|
+
explicitGraphEnabled: this.explicitGraphEnabled
|
|
187
|
+
});
|
|
188
188
|
}
|
|
189
189
|
}
|
|
190
190
|
function reactPlugin(options) {
|
package/src/router-adapter.d.ts
CHANGED
|
@@ -12,11 +12,10 @@
|
|
|
12
12
|
* const myRouter: ReactRouterAdapter = {
|
|
13
13
|
* name: 'my-router',
|
|
14
14
|
* bundle: {
|
|
15
|
-
* importPath: '@my/router/browser
|
|
15
|
+
* importPath: '@my/router/browser',
|
|
16
16
|
* outputName: 'my-router',
|
|
17
17
|
* externals: ['react', 'react-dom'],
|
|
18
18
|
* },
|
|
19
|
-
* importMapKey: '@my/router',
|
|
20
19
|
* components: {
|
|
21
20
|
* router: 'MyRouter',
|
|
22
21
|
* pageContent: 'PageOutlet',
|
|
@@ -36,39 +35,33 @@ export interface ReactRouterAdapter {
|
|
|
36
35
|
bundle: {
|
|
37
36
|
/**
|
|
38
37
|
* Node module import path for the browser-compatible entry.
|
|
39
|
-
* @example '@ecopages/react-router/browser
|
|
38
|
+
* @example '@ecopages/react-router/browser'
|
|
40
39
|
*/
|
|
41
40
|
importPath: string;
|
|
42
41
|
/**
|
|
43
42
|
* Output filename (without extension).
|
|
44
|
-
* @example '
|
|
43
|
+
* @example 'my-router'
|
|
45
44
|
*/
|
|
46
45
|
outputName: string;
|
|
47
46
|
/**
|
|
48
47
|
* Packages to externalize when bundling.
|
|
49
|
-
* These should
|
|
50
|
-
* @example ['react', 'react-dom'
|
|
48
|
+
* These should stay as bare runtime dependencies for the router bundle.
|
|
49
|
+
* @example ['react', 'react-dom']
|
|
51
50
|
*/
|
|
52
51
|
externals: string[];
|
|
53
52
|
};
|
|
54
|
-
/**
|
|
55
|
-
* Bare specifier for the runtime mapping entry.
|
|
56
|
-
* This is what the hydration script will import from.
|
|
57
|
-
* @example '@ecopages/react-router'
|
|
58
|
-
*/
|
|
59
|
-
importMapKey: string;
|
|
60
53
|
/**
|
|
61
54
|
* Component names to import from the router package.
|
|
62
55
|
*/
|
|
63
56
|
components: {
|
|
64
57
|
/**
|
|
65
58
|
* The router component that wraps the layout.
|
|
66
|
-
* @example '
|
|
59
|
+
* @example 'MyRouter'
|
|
67
60
|
*/
|
|
68
61
|
router: string;
|
|
69
62
|
/**
|
|
70
63
|
* The component that renders the current page content.
|
|
71
|
-
* @example '
|
|
64
|
+
* @example 'PageOutlet'
|
|
72
65
|
*/
|
|
73
66
|
pageContent: string;
|
|
74
67
|
};
|
|
@@ -53,5 +53,5 @@ export declare class ReactBundleService {
|
|
|
53
53
|
* Creates the esbuild plugin that rewrites bare React specifiers
|
|
54
54
|
* to their runtime asset URLs.
|
|
55
55
|
*/
|
|
56
|
-
createRuntimeAliasPlugin(
|
|
56
|
+
createRuntimeAliasPlugin(runtimeAliasMap: Record<string, string>): import("@ecopages/core/build/build-types").EcoBuildPlugin | null;
|
|
57
57
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
buildReactRuntimeAliasMap,
|
|
4
4
|
getReactClientGraphAllowSpecifiers,
|
|
5
5
|
getReactRuntimeExternalSpecifiers
|
|
6
|
-
} from "../utils/react-runtime-
|
|
6
|
+
} from "../utils/react-runtime-alias-map.js";
|
|
7
7
|
import { createUseSyncExternalStoreShimPlugin } from "../utils/use-sync-external-store-shim-plugin.js";
|
|
8
8
|
import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
|
|
9
9
|
import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
|
|
@@ -33,6 +33,7 @@ class ReactBundleService {
|
|
|
33
33
|
* @returns Bundle options object for the build adapter
|
|
34
34
|
*/
|
|
35
35
|
async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
|
|
36
|
+
const runtimeImports = this.getRuntimeImports();
|
|
36
37
|
const options = {
|
|
37
38
|
mainFields: ["module", "browser", "main"],
|
|
38
39
|
naming: `${componentName}.[ext]`,
|
|
@@ -43,7 +44,10 @@ class ReactBundleService {
|
|
|
43
44
|
}
|
|
44
45
|
};
|
|
45
46
|
if (!bundleOptions.includeRuntime) {
|
|
46
|
-
options.external =
|
|
47
|
+
options.external = [
|
|
48
|
+
...getReactRuntimeExternalSpecifiers(),
|
|
49
|
+
...Object.values(runtimeImports).filter((specifier) => Boolean(specifier))
|
|
50
|
+
];
|
|
47
51
|
}
|
|
48
52
|
const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
|
|
49
53
|
absWorkingDir: this.config.rootDir,
|
|
@@ -59,11 +63,7 @@ class ReactBundleService {
|
|
|
59
63
|
name: "react-renderer-use-sync-external-store-shim",
|
|
60
64
|
namespace: "ecopages-react-renderer-shim"
|
|
61
65
|
});
|
|
62
|
-
const runtimePlugins = bundleOptions.includeRuntime ? [] : [
|
|
63
|
-
this.createRuntimeAliasPlugin(
|
|
64
|
-
buildReactRuntimeSpecifierMap(this.getRuntimeImports(), this.config.routerAdapter)
|
|
65
|
-
)
|
|
66
|
-
];
|
|
66
|
+
const runtimePlugins = bundleOptions.includeRuntime ? [] : [this.createRuntimeAliasPlugin(buildReactRuntimeAliasMap(runtimeImports))];
|
|
67
67
|
if (isMdx && this.config.mdxCompilerOptions) {
|
|
68
68
|
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
69
69
|
const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
|
|
@@ -88,8 +88,8 @@ class ReactBundleService {
|
|
|
88
88
|
* Creates the esbuild plugin that rewrites bare React specifiers
|
|
89
89
|
* to their runtime asset URLs.
|
|
90
90
|
*/
|
|
91
|
-
createRuntimeAliasPlugin(
|
|
92
|
-
return createRuntimeSpecifierAliasPlugin(
|
|
91
|
+
createRuntimeAliasPlugin(runtimeAliasMap) {
|
|
92
|
+
return createRuntimeSpecifierAliasPlugin(runtimeAliasMap, { name: "react-runtime-import-alias" });
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
export {
|
|
@@ -51,7 +51,7 @@ export declare class ReactHydrationAssetService {
|
|
|
51
51
|
* @param isMdx - Whether the source file is an MDX file
|
|
52
52
|
* @returns One page-owned asset definition for processing
|
|
53
53
|
*/
|
|
54
|
-
createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, isMdx: boolean): AssetDefinition[];
|
|
54
|
+
createPageDependencies(pagePath: string, componentName: string, importPath: string, pageModuleUrlExpression: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, useBrowserRuntimeImports: boolean, isMdx: boolean): AssetDefinition[];
|
|
55
55
|
/**
|
|
56
56
|
* Builds client-side assets for a React component island.
|
|
57
57
|
*
|
|
@@ -63,12 +63,12 @@ export declare class ReactHydrationAssetService {
|
|
|
63
63
|
*/
|
|
64
64
|
buildComponentRenderAssets(componentFile: string, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
|
|
65
65
|
/**
|
|
66
|
-
* Builds
|
|
66
|
+
* Builds the Page Browser Graph assets for a React page.
|
|
67
67
|
*
|
|
68
68
|
* @param pagePath - Absolute file path of the page
|
|
69
69
|
* @param isMdx - Whether the page is an MDX file
|
|
70
70
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
71
71
|
* @returns Processed assets for the route
|
|
72
72
|
*/
|
|
73
|
-
|
|
73
|
+
buildPageBrowserGraphAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
|
|
74
74
|
}
|
|
@@ -46,16 +46,17 @@ class ReactHydrationAssetService {
|
|
|
46
46
|
* @param isMdx - Whether the source file is an MDX file
|
|
47
47
|
* @returns One page-owned asset definition for processing
|
|
48
48
|
*/
|
|
49
|
-
createPageDependencies(pagePath, componentName, importPath, bundleOptions, isDevelopment, isMdx) {
|
|
49
|
+
createPageDependencies(pagePath, componentName, importPath, pageModuleUrlExpression, bundleOptions, isDevelopment, useBrowserRuntimeImports, isMdx) {
|
|
50
50
|
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
51
51
|
return [
|
|
52
52
|
AssetFactory.createContentScript({
|
|
53
53
|
position: "head",
|
|
54
54
|
content: createHydrationScript({
|
|
55
55
|
importPath: isDevelopment ? importPath : pagePath,
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
pageModuleUrlExpression,
|
|
57
|
+
reactImportPath: useBrowserRuntimeImports ? runtimeImports.react : "react",
|
|
58
|
+
reactDomClientImportPath: useBrowserRuntimeImports ? runtimeImports.reactDomClient : "react-dom/client",
|
|
59
|
+
routerImportPath: useBrowserRuntimeImports ? runtimeImports.router : this.config.routerAdapter?.bundle.importPath,
|
|
59
60
|
isDevelopment,
|
|
60
61
|
isMdx,
|
|
61
62
|
router: this.config.routerAdapter,
|
|
@@ -145,32 +146,38 @@ class ReactHydrationAssetService {
|
|
|
145
146
|
return this.config.assetProcessingService.processDependencies(dependencies, componentName);
|
|
146
147
|
}
|
|
147
148
|
/**
|
|
148
|
-
* Builds
|
|
149
|
+
* Builds the Page Browser Graph assets for a React page.
|
|
149
150
|
*
|
|
150
151
|
* @param pagePath - Absolute file path of the page
|
|
151
152
|
* @param isMdx - Whether the page is an MDX file
|
|
152
153
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
153
154
|
* @returns Processed assets for the route
|
|
154
155
|
*/
|
|
155
|
-
async
|
|
156
|
+
async buildPageBrowserGraphAssets(pagePath, isMdx, declaredModules) {
|
|
156
157
|
const componentName = `ecopages-react-${rapidhash(pagePath)}`;
|
|
157
158
|
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
158
159
|
const isDevelopment = hmrManager?.isEnabled() ?? false;
|
|
160
|
+
const isHostedDevelopment = !isDevelopment && process.env.NODE_ENV !== "production";
|
|
161
|
+
const useBrowserRuntimeImports = isDevelopment || isHostedDevelopment;
|
|
159
162
|
if (isDevelopment) {
|
|
160
163
|
this.config.hmrPageMetadataCache?.setDeclaredModules(pagePath, declaredModules);
|
|
161
164
|
}
|
|
162
165
|
const importPath = await this.resolveAssetImportPath(pagePath, componentName);
|
|
166
|
+
const pageModuleUrlExpression = "import.meta.url";
|
|
163
167
|
const bundleOptions = await this.config.bundleService.createBundleOptions(
|
|
164
168
|
componentName,
|
|
165
169
|
isMdx,
|
|
166
|
-
declaredModules
|
|
170
|
+
declaredModules,
|
|
171
|
+
{ includeRuntime: !useBrowserRuntimeImports }
|
|
167
172
|
);
|
|
168
173
|
const dependencies = this.createPageDependencies(
|
|
169
174
|
pagePath,
|
|
170
175
|
componentName,
|
|
171
176
|
importPath,
|
|
177
|
+
pageModuleUrlExpression,
|
|
172
178
|
bundleOptions,
|
|
173
179
|
isDevelopment,
|
|
180
|
+
useBrowserRuntimeImports,
|
|
174
181
|
isMdx
|
|
175
182
|
);
|
|
176
183
|
if (!this.config.assetProcessingService) {
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import type { EcoBuildPlugin } from '@ecopages/core/
|
|
9
|
+
import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
10
10
|
import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
11
11
|
import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
12
12
|
export type ReactRuntimeImports = {
|
|
@@ -33,7 +33,7 @@ export declare class ReactRuntimeBundleService {
|
|
|
33
33
|
private getReactDomVendorFileName;
|
|
34
34
|
private getRouterVendorFileName;
|
|
35
35
|
getRuntimeImports(mode?: RuntimeMode): ReactRuntimeImports;
|
|
36
|
-
|
|
36
|
+
getRuntimeAliasMap(mode?: RuntimeMode): Record<string, string>;
|
|
37
37
|
getDependencies(): AssetDefinition[];
|
|
38
38
|
createRuntimeAliasPlugin(mode?: RuntimeMode): EcoBuildPlugin;
|
|
39
39
|
}
|
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
createBrowserRuntimeScriptAsset
|
|
6
6
|
} from "@ecopages/core/services/asset-processing-service";
|
|
7
7
|
import { createReactDomRuntimeInteropPlugin } from "../utils/react-dom-runtime-interop-plugin.js";
|
|
8
|
-
import {
|
|
8
|
+
import { buildReactRuntimeAliasMap } from "../utils/react-runtime-alias-map.js";
|
|
9
9
|
class ReactRuntimeBundleService {
|
|
10
10
|
config;
|
|
11
11
|
constructor(config) {
|
|
@@ -54,8 +54,8 @@ class ReactRuntimeBundleService {
|
|
|
54
54
|
}
|
|
55
55
|
return runtimeImports;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
return
|
|
57
|
+
getRuntimeAliasMap(mode = this.getCurrentRuntimeMode()) {
|
|
58
|
+
return buildReactRuntimeAliasMap(this.getRuntimeImports(mode));
|
|
59
59
|
}
|
|
60
60
|
getDependencies() {
|
|
61
61
|
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
|
|
@@ -71,7 +71,7 @@ class ReactRuntimeBundleService {
|
|
|
71
71
|
(plugin) => plugin !== null
|
|
72
72
|
);
|
|
73
73
|
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
|
|
74
|
-
const mappedSpecifiers = new Set(Object.keys(this.
|
|
74
|
+
const mappedSpecifiers = new Set(Object.keys(this.getRuntimeAliasMap(mode)));
|
|
75
75
|
dependencies.push(
|
|
76
76
|
createBrowserRuntimeModuleAsset({
|
|
77
77
|
modules: [
|
|
@@ -120,7 +120,7 @@ class ReactRuntimeBundleService {
|
|
|
120
120
|
return dependencies;
|
|
121
121
|
}
|
|
122
122
|
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
|
|
123
|
-
return createRuntimeSpecifierAliasPlugin(this.
|
|
123
|
+
return createRuntimeSpecifierAliasPlugin(this.getRuntimeAliasMap(mode), {
|
|
124
124
|
name: `react-plugin-runtime-alias-${mode}`
|
|
125
125
|
});
|
|
126
126
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Additionally, this plugin provides a build-time transform that statically resolves and
|
|
15
15
|
* inlines `fs.readFileSync(path.resolve(...))` calls to prevent server/client data mismatches.
|
|
16
16
|
*/
|
|
17
|
-
import type { EcoBuildPlugin } from '@ecopages/core/
|
|
17
|
+
import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
18
18
|
/**
|
|
19
19
|
* Configuration options for the Client Graph Boundary esbuild plugin.
|
|
20
20
|
*
|
|
@@ -10,6 +10,8 @@ import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
|
10
10
|
export type HydrationScriptOptions = {
|
|
11
11
|
/** The module path imported by the page entry module. */
|
|
12
12
|
importPath: string;
|
|
13
|
+
/** Browser expression that resolves to the page module URL the router should import. */
|
|
14
|
+
pageModuleUrlExpression?: string;
|
|
13
15
|
/** Stable id of the page entry script tag in the document. */
|
|
14
16
|
scriptId: string;
|
|
15
17
|
/** Direct import path for React runtime module */
|
|
@@ -60,6 +60,7 @@ function getProdReuseExistingRouterRootScript() {
|
|
|
60
60
|
}
|
|
61
61
|
function createDevScriptWithRouter(options) {
|
|
62
62
|
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
63
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
63
64
|
const { components, getRouterProps } = router;
|
|
64
65
|
if (!routerImportPath) {
|
|
65
66
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
@@ -69,7 +70,7 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
|
69
70
|
import { createElement } from "${reactImportPath}";
|
|
70
71
|
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
71
72
|
${getImportStatement(importPath, isMdx)}
|
|
72
|
-
const pageModuleUrl =
|
|
73
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
73
74
|
export default Page;
|
|
74
75
|
export const config = Page.config;
|
|
75
76
|
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
@@ -157,11 +158,12 @@ if (document.readyState === "loading") {
|
|
|
157
158
|
}
|
|
158
159
|
function createDevScriptWithoutRouter(options) {
|
|
159
160
|
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
161
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
160
162
|
return `
|
|
161
163
|
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
162
164
|
import { createElement } from "${reactImportPath}";
|
|
163
165
|
${getImportStatement(importPath, isMdx)}
|
|
164
|
-
const pageModuleUrl =
|
|
166
|
+
const pageModuleUrl = ${pageModuleUrlExpression};
|
|
165
167
|
export default Page;
|
|
166
168
|
export const config = Page.config;
|
|
167
169
|
const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
|
|
@@ -229,21 +231,23 @@ if (document.readyState === "loading") {
|
|
|
229
231
|
}
|
|
230
232
|
function createProdScriptWithRouter(options) {
|
|
231
233
|
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
|
|
234
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
232
235
|
const { components, getRouterProps } = router;
|
|
233
236
|
if (!routerImportPath) {
|
|
234
237
|
throw new Error("routerImportPath is required when router adapter is configured");
|
|
235
238
|
}
|
|
236
239
|
if (isMdx) {
|
|
237
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u
|
|
240
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
238
241
|
}
|
|
239
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const u
|
|
242
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
240
243
|
}
|
|
241
244
|
function createProdScriptWithoutRouter(options) {
|
|
242
245
|
const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
|
|
246
|
+
const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
|
|
243
247
|
if (isMdx) {
|
|
244
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u
|
|
248
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
245
249
|
}
|
|
246
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u
|
|
250
|
+
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
|
|
247
251
|
}
|
|
248
252
|
function createHydrationScript(options) {
|
|
249
253
|
const { isDevelopment, router } = options;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import type { EcoBuildPlugin } from '@ecopages/core/
|
|
1
|
+
import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
2
2
|
import { type CompileOptions } from '@mdx-js/mdx';
|
|
3
3
|
export declare function createReactMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
2
2
|
import type { ReactRuntimeImports } from '../services/react-runtime-bundle.service.js';
|
|
3
3
|
export declare const REACT_RUNTIME_SPECIFIERS: readonly ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"];
|
|
4
|
-
export declare function
|
|
4
|
+
export declare function buildReactRuntimeAliasMap(runtimeImports: ReactRuntimeImports): Record<string, string>;
|
|
5
5
|
export declare function getReactRuntimeExternalSpecifiers(): string[];
|
|
6
6
|
export declare function getReactClientGraphAllowSpecifiers(runtimeSpecifiers: Iterable<string>, routerAdapter?: ReactRouterAdapter): string[];
|
|
@@ -5,18 +5,14 @@ const REACT_RUNTIME_SPECIFIERS = [
|
|
|
5
5
|
"react/jsx-dev-runtime",
|
|
6
6
|
"react-dom/client"
|
|
7
7
|
];
|
|
8
|
-
function
|
|
9
|
-
|
|
8
|
+
function buildReactRuntimeAliasMap(runtimeImports) {
|
|
9
|
+
return {
|
|
10
10
|
react: runtimeImports.react,
|
|
11
11
|
"react/jsx-runtime": runtimeImports.reactJsxRuntime,
|
|
12
12
|
"react/jsx-dev-runtime": runtimeImports.reactJsxDevRuntime,
|
|
13
13
|
"react-dom": runtimeImports.reactDom,
|
|
14
14
|
"react-dom/client": runtimeImports.reactDomClient
|
|
15
15
|
};
|
|
16
|
-
if (routerAdapter && runtimeImports.router) {
|
|
17
|
-
map[routerAdapter.importMapKey] = runtimeImports.router;
|
|
18
|
-
}
|
|
19
|
-
return map;
|
|
20
16
|
}
|
|
21
17
|
function getReactRuntimeExternalSpecifiers() {
|
|
22
18
|
return [...REACT_RUNTIME_SPECIFIERS];
|
|
@@ -25,13 +21,13 @@ function getReactClientGraphAllowSpecifiers(runtimeSpecifiers, routerAdapter) {
|
|
|
25
21
|
return [
|
|
26
22
|
"@ecopages/core",
|
|
27
23
|
...REACT_RUNTIME_SPECIFIERS,
|
|
28
|
-
...routerAdapter ? [routerAdapter.
|
|
24
|
+
...routerAdapter ? [routerAdapter.bundle.importPath] : [],
|
|
29
25
|
...Array.from(runtimeSpecifiers)
|
|
30
26
|
];
|
|
31
27
|
}
|
|
32
28
|
export {
|
|
33
29
|
REACT_RUNTIME_SPECIFIERS,
|
|
34
|
-
|
|
30
|
+
buildReactRuntimeAliasMap,
|
|
35
31
|
getReactClientGraphAllowSpecifiers,
|
|
36
32
|
getReactRuntimeExternalSpecifiers
|
|
37
33
|
};
|