@ecopages/react 0.2.0-alpha.12 → 0.2.0-alpha.14
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 +5 -7
- package/README.md +10 -0
- package/package.json +3 -3
- package/src/react-renderer.d.ts +15 -31
- package/src/react-renderer.js +146 -154
- package/src/react.plugin.d.ts +0 -12
- package/src/react.plugin.js +0 -13
package/CHANGELOG.md
CHANGED
|
@@ -8,11 +8,7 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
8
8
|
|
|
9
9
|
### Bug Fixes
|
|
10
10
|
|
|
11
|
-
- Fixed
|
|
12
|
-
- Fixed React page and MDX module loading to use host-provided loaders on Vite or Nitro and a lightweight browser `eco` shim in preview and build output.
|
|
13
|
-
- Fixed React Fast Refresh to keep React-owned island entrypoints on the React HMR path while ignoring non-React watched script entrypoints.
|
|
14
|
-
- Fixed `renderDocument` to prepend `<!DOCTYPE html>` for both React-managed and non-React HTML templates, matching the behavior of all other integrations.
|
|
15
|
-
- Fixed React island asset generation to share both bundled component modules and hydration bootstraps across repeated island instances of the same component.
|
|
11
|
+
- Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer boundary resolution across Bun, Vite, and Nitro flows.
|
|
16
12
|
|
|
17
13
|
### Features
|
|
18
14
|
|
|
@@ -22,10 +18,12 @@ All notable changes to `@ecopages/react` are documented here.
|
|
|
22
18
|
|
|
23
19
|
- Consolidated React bundling, hydration, and runtime state behind shared service boundaries and `window.__ECO_PAGES__`.
|
|
24
20
|
|
|
21
|
+
### Documentation
|
|
22
|
+
|
|
23
|
+
- Updated the README to document React-owned mixed boundaries and React MDX setup.
|
|
24
|
+
|
|
25
25
|
---
|
|
26
26
|
|
|
27
27
|
## Migration Notes
|
|
28
28
|
|
|
29
|
-
- The React integration now requires explicit client boundary declarations for client-rendered components.
|
|
30
29
|
- React MDX support is built in and no longer requires installing `@ecopages/mdx` just to enable React MDX routes.
|
|
31
|
-
- The internal service layer is not part of the public API and may change between releases.
|
package/README.md
CHANGED
|
@@ -60,6 +60,16 @@ const config = await new ConfigBuilder()
|
|
|
60
60
|
export default config;
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
+
## Mixed Rendering
|
|
64
|
+
|
|
65
|
+
The React integration can participate in mixed-renderer apps in three ways:
|
|
66
|
+
|
|
67
|
+
- React can own the page or view directly.
|
|
68
|
+
- React can render nested component boundaries inside pages owned by another integration.
|
|
69
|
+
- React can render through non-React page, layout, or document shells when those shell components return strings.
|
|
70
|
+
|
|
71
|
+
When a non-React render pass enters a React-owned boundary, Ecopages hands that boundary 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
|
+
|
|
63
73
|
## Server and Client Graph Contract
|
|
64
74
|
|
|
65
75
|
The React integration supports Node.js modules and server-only code **only on the server execution graph**.
|
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.14",
|
|
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.14",
|
|
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.14",
|
|
64
64
|
"@ecopages/logger": "^0.2.3",
|
|
65
65
|
"@mdx-js/esbuild": "^3.0.1",
|
|
66
66
|
"@mdx-js/mdx": "^3.1.0",
|
package/src/react-renderer.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
77
77
|
*
|
|
78
78
|
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
79
79
|
* page-data contract as fully React-owned documents so navigation and hydration
|
|
80
|
-
* can read one
|
|
80
|
+
* can read one shared document payload consistently.
|
|
81
81
|
*/
|
|
82
82
|
private buildRouterPageDataScript;
|
|
83
83
|
private getRouterDocumentAttributes;
|
|
@@ -111,14 +111,6 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
111
111
|
* HTML transformer state.
|
|
112
112
|
*/
|
|
113
113
|
private appendHydrationAssetsForFile;
|
|
114
|
-
/**
|
|
115
|
-
* Resolves metadata for direct `renderToResponse()` calls.
|
|
116
|
-
*
|
|
117
|
-
* View rendering bypasses the normal route-file pipeline, so metadata has to be
|
|
118
|
-
* evaluated here from either the component-level generator or the application
|
|
119
|
-
* default.
|
|
120
|
-
*/
|
|
121
|
-
private resolveViewMetadata;
|
|
122
114
|
/**
|
|
123
115
|
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
124
116
|
* return serialized HTML.
|
|
@@ -128,35 +120,23 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
128
120
|
*/
|
|
129
121
|
private renderNonReactShellComponent;
|
|
130
122
|
/**
|
|
131
|
-
* Renders one React component boundary
|
|
123
|
+
* Renders one React component boundary while preserving already-resolved child HTML.
|
|
132
124
|
*
|
|
133
|
-
* When
|
|
134
|
-
* the child payload must remain raw SSR output rather than a React
|
|
135
|
-
* child, otherwise React would escape it. This helper renders a unique
|
|
136
|
-
* through React and swaps that token back to the resolved HTML
|
|
125
|
+
* When nested boundary resolution has already produced child HTML for this
|
|
126
|
+
* boundary, the child payload must remain raw SSR output rather than a React
|
|
127
|
+
* string child, otherwise React would escape it. This helper renders a unique
|
|
128
|
+
* token through React and swaps that token back to the resolved HTML
|
|
129
|
+
* afterward.
|
|
137
130
|
*
|
|
138
|
-
* @param input Component render input
|
|
131
|
+
* @param input Component render input for the current boundary.
|
|
139
132
|
* @param context React-specific render context for stable token generation.
|
|
140
133
|
* @returns Serialized component HTML with resolved child markup preserved.
|
|
141
134
|
*/
|
|
142
135
|
private renderComponentHtml;
|
|
136
|
+
private restoreRuntimeChildHtml;
|
|
137
|
+
private renderQueuedChildrenToHtml;
|
|
138
|
+
private resolveQueuedBoundaryHtml;
|
|
143
139
|
private buildHydrationProps;
|
|
144
|
-
/**
|
|
145
|
-
* Produces the page body before the final HTML template is applied.
|
|
146
|
-
*
|
|
147
|
-
* This method owns the React/non-React layout split. React-managed layouts stay
|
|
148
|
-
* as React elements so they can stream normally; non-React layouts are rendered
|
|
149
|
-
* to HTML first and then passed through as serialized content.
|
|
150
|
-
*/
|
|
151
|
-
private composePageContent;
|
|
152
|
-
/**
|
|
153
|
-
* Wraps composed page content in the final document template.
|
|
154
|
-
*
|
|
155
|
-
* React-owned HTML templates stream directly. Non-React templates receive
|
|
156
|
-
* pre-rendered page HTML plus the canonical React page-data payload so the
|
|
157
|
-
* client runtime can recover page data after cross-integration handoff.
|
|
158
|
-
*/
|
|
159
|
-
private renderDocument;
|
|
160
140
|
/**
|
|
161
141
|
* Renders a React component for component-level orchestration.
|
|
162
142
|
*
|
|
@@ -172,6 +152,10 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
|
|
|
172
152
|
* deterministic mount target per component instance.
|
|
173
153
|
*/
|
|
174
154
|
renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
|
|
155
|
+
protected createComponentBoundaryRuntime(options: {
|
|
156
|
+
boundaryInput: ComponentRenderInput;
|
|
157
|
+
rendererCache: Map<string, IntegrationRenderer<any>>;
|
|
158
|
+
}): import("@ecopages/core").ComponentBoundaryRuntime;
|
|
175
159
|
/**
|
|
176
160
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
177
161
|
* @param filePath - The file path to check
|
package/src/react-renderer.js
CHANGED
|
@@ -6,32 +6,14 @@ import { rapidhash } from "@ecopages/core/hash";
|
|
|
6
6
|
import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
|
|
7
7
|
import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { createElement } from "react";
|
|
9
|
+
import { createElement, Fragment } from "react";
|
|
10
10
|
import { renderToReadableStream, renderToString } from "react-dom/server";
|
|
11
11
|
import { PLUGIN_NAME } from "./react.plugin.js";
|
|
12
12
|
import { hasSingleRootElement } from "./utils/html-boundary.js";
|
|
13
13
|
import { ReactBundleService } from "./services/react-bundle.service.js";
|
|
14
14
|
import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
|
|
15
15
|
import { ReactPageModuleService } from "./services/react-page-module.service.js";
|
|
16
|
-
import {
|
|
17
|
-
getReactIslandComponentKey,
|
|
18
|
-
ReactHydrationAssetService
|
|
19
|
-
} from "./services/react-hydration-asset.service.js";
|
|
20
|
-
function decodeHtmlEntities(value) {
|
|
21
|
-
let decoded = value;
|
|
22
|
-
let previous;
|
|
23
|
-
do {
|
|
24
|
-
previous = decoded;
|
|
25
|
-
decoded = decoded.replaceAll(""", '"').replaceAll("'", "'").replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
26
|
-
} while (decoded !== previous);
|
|
27
|
-
return decoded;
|
|
28
|
-
}
|
|
29
|
-
function restoreEscapedComponentMarkers(html) {
|
|
30
|
-
return html.replace(
|
|
31
|
-
/&(?:amp;)?lt;eco-marker\b[\s\S]*?&(?:amp;)?gt;&(?:amp;)?lt;\/eco-marker&(?:amp;)?gt;/g,
|
|
32
|
-
(marker) => decodeHtmlEntities(marker)
|
|
33
|
-
);
|
|
34
|
-
}
|
|
16
|
+
import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
|
|
35
17
|
class ReactRenderError extends Error {
|
|
36
18
|
constructor(message) {
|
|
37
19
|
super(message);
|
|
@@ -124,7 +106,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
124
106
|
*
|
|
125
107
|
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
126
108
|
* page-data contract as fully React-owned documents so navigation and hydration
|
|
127
|
-
* can read one
|
|
109
|
+
* can read one shared document payload consistently.
|
|
128
110
|
*/
|
|
129
111
|
buildRouterPageDataScript(pageProps) {
|
|
130
112
|
const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
|
|
@@ -183,25 +165,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
183
165
|
return;
|
|
184
166
|
}
|
|
185
167
|
const hydrationAssets = await this.buildRouteRenderAssets(filePath);
|
|
186
|
-
this.
|
|
187
|
-
...this.htmlTransformer.getProcessedDependencies(),
|
|
188
|
-
...hydrationAssets
|
|
189
|
-
]);
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Resolves metadata for direct `renderToResponse()` calls.
|
|
193
|
-
*
|
|
194
|
-
* View rendering bypasses the normal route-file pipeline, so metadata has to be
|
|
195
|
-
* evaluated here from either the component-level generator or the application
|
|
196
|
-
* default.
|
|
197
|
-
*/
|
|
198
|
-
async resolveViewMetadata(view, props) {
|
|
199
|
-
return view.metadata ? await view.metadata({
|
|
200
|
-
params: {},
|
|
201
|
-
query: {},
|
|
202
|
-
props,
|
|
203
|
-
appConfig: this.appConfig
|
|
204
|
-
}) : this.appConfig.defaultMetadata;
|
|
168
|
+
this.appendProcessedDependencies(hydrationAssets);
|
|
205
169
|
}
|
|
206
170
|
/**
|
|
207
171
|
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
@@ -218,100 +182,77 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
218
182
|
throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
|
|
219
183
|
}
|
|
220
184
|
/**
|
|
221
|
-
* Renders one React component boundary
|
|
185
|
+
* Renders one React component boundary while preserving already-resolved child HTML.
|
|
222
186
|
*
|
|
223
|
-
* When
|
|
224
|
-
* the child payload must remain raw SSR output rather than a React
|
|
225
|
-
* child, otherwise React would escape it. This helper renders a unique
|
|
226
|
-
* through React and swaps that token back to the resolved HTML
|
|
187
|
+
* When nested boundary resolution has already produced child HTML for this
|
|
188
|
+
* boundary, the child payload must remain raw SSR output rather than a React
|
|
189
|
+
* string child, otherwise React would escape it. This helper renders a unique
|
|
190
|
+
* token through React and swaps that token back to the resolved HTML
|
|
191
|
+
* afterward.
|
|
227
192
|
*
|
|
228
|
-
* @param input Component render input
|
|
193
|
+
* @param input Component render input for the current boundary.
|
|
229
194
|
* @param context React-specific render context for stable token generation.
|
|
230
195
|
* @returns Serialized component HTML with resolved child markup preserved.
|
|
231
196
|
*/
|
|
232
|
-
renderComponentHtml(input, context) {
|
|
197
|
+
renderComponentHtml(input, context, runtimeContext) {
|
|
233
198
|
if (input.children === void 0) {
|
|
234
|
-
return
|
|
199
|
+
return this.normalizeBoundaryArtifactHtml(
|
|
235
200
|
renderToString(createElement(this.asReactComponent(input.component), input.props))
|
|
236
201
|
);
|
|
237
202
|
}
|
|
238
203
|
const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
|
|
239
204
|
const rawChildrenToken = `__ECO_RAW_HTML_CHILD_${context.componentInstanceId ?? "component"}__`;
|
|
205
|
+
if (runtimeContext) {
|
|
206
|
+
runtimeContext.rawChildrenToken = rawChildrenToken;
|
|
207
|
+
runtimeContext.rawChildrenHtml = resolvedChildHtml;
|
|
208
|
+
}
|
|
240
209
|
const html = renderToString(
|
|
241
210
|
createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
242
211
|
);
|
|
243
|
-
return
|
|
212
|
+
return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
244
213
|
}
|
|
245
|
-
|
|
246
|
-
if (!
|
|
247
|
-
return
|
|
214
|
+
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
215
|
+
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
216
|
+
return html;
|
|
248
217
|
}
|
|
249
|
-
|
|
250
|
-
return hydrationProps;
|
|
218
|
+
return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
|
|
251
219
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
* This method owns the React/non-React layout split. React-managed layouts stay
|
|
256
|
-
* as React elements so they can stream normally; non-React layouts are rendered
|
|
257
|
-
* to HTML first and then passed through as serialized content.
|
|
258
|
-
*/
|
|
259
|
-
async composePageContent(options) {
|
|
260
|
-
const pageElement = createElement(options.Page, options.pageProps);
|
|
261
|
-
const pageHtml = restoreEscapedComponentMarkers(renderToString(pageElement));
|
|
262
|
-
const layoutProps = options.locals ? { locals: options.locals } : {};
|
|
263
|
-
if (!options.Layout) {
|
|
264
|
-
return { contentNode: pageElement, contentHtml: pageHtml };
|
|
265
|
-
}
|
|
266
|
-
if (this.isReactManagedComponent(options.Layout)) {
|
|
267
|
-
const layoutElement = createElement(this.asReactComponent(options.Layout), layoutProps, pageElement);
|
|
268
|
-
return {
|
|
269
|
-
contentNode: layoutElement,
|
|
270
|
-
contentHtml: restoreEscapedComponentMarkers(renderToString(layoutElement))
|
|
271
|
-
};
|
|
220
|
+
async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
|
|
221
|
+
if (children === void 0) {
|
|
222
|
+
return void 0;
|
|
272
223
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
{ ...layoutProps, children: pageHtml },
|
|
276
|
-
"Layout"
|
|
224
|
+
let html = this.normalizeBoundaryArtifactHtml(
|
|
225
|
+
renderToString(createElement(Fragment, null, children))
|
|
277
226
|
);
|
|
278
|
-
|
|
227
|
+
html = this.restoreRuntimeChildHtml(html, runtimeContext);
|
|
228
|
+
html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
|
|
229
|
+
return html;
|
|
279
230
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
return this.DOC_TYPE + html.split(rawChildrenToken).join(options.contentHtml);
|
|
231
|
+
async resolveQueuedBoundaryHtml(html, runtimeContext) {
|
|
232
|
+
return this.resolveRendererOwnedQueuedBoundaryHtml({
|
|
233
|
+
html,
|
|
234
|
+
runtimeContext,
|
|
235
|
+
queueLabel: "React",
|
|
236
|
+
renderQueuedChildren: async (children, currentRuntimeContext, queuedResolutionsByToken, resolveToken) => {
|
|
237
|
+
const renderedHtml = await this.renderQueuedChildrenToHtml(
|
|
238
|
+
children,
|
|
239
|
+
currentRuntimeContext,
|
|
240
|
+
queuedResolutionsByToken,
|
|
241
|
+
resolveToken
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
assets: [],
|
|
245
|
+
html: renderedHtml
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
buildHydrationProps(props) {
|
|
251
|
+
if (!props || !Object.prototype.hasOwnProperty.call(props, "locals")) {
|
|
252
|
+
return props ?? {};
|
|
303
253
|
}
|
|
304
|
-
const
|
|
305
|
-
return
|
|
306
|
-
this.asNonReactShellComponent(options.HtmlTemplate),
|
|
307
|
-
{
|
|
308
|
-
metadata: options.metadata,
|
|
309
|
-
pageProps: options.pageProps,
|
|
310
|
-
children: options.contentHtml,
|
|
311
|
-
headContent
|
|
312
|
-
},
|
|
313
|
-
"HtmlTemplate"
|
|
314
|
-
);
|
|
254
|
+
const { locals: _locals, ...hydrationProps } = props;
|
|
255
|
+
return hydrationProps;
|
|
315
256
|
}
|
|
316
257
|
/**
|
|
317
258
|
* Renders a React component for component-level orchestration.
|
|
@@ -328,10 +269,42 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
328
269
|
* deterministic mount target per component instance.
|
|
329
270
|
*/
|
|
330
271
|
async renderComponent(input) {
|
|
272
|
+
const runtimeContext = this.getQueuedBoundaryRuntime(input);
|
|
273
|
+
if (!this.isReactManagedComponent(input.component)) {
|
|
274
|
+
let props = input.props;
|
|
275
|
+
if (input.children !== void 0) {
|
|
276
|
+
props = {
|
|
277
|
+
...input.props,
|
|
278
|
+
children: typeof input.children === "string" ? input.children : String(input.children ?? "")
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const html2 = await this.renderNonReactShellComponent(
|
|
282
|
+
this.asNonReactShellComponent(input.component),
|
|
283
|
+
props,
|
|
284
|
+
"Component"
|
|
285
|
+
);
|
|
286
|
+
const hasDependencies = Boolean(input.component.config?.dependencies);
|
|
287
|
+
const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
|
|
288
|
+
const assets2 = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
|
|
289
|
+
const queuedBoundaryResolution2 = await this.resolveQueuedBoundaryHtml(html2, runtimeContext);
|
|
290
|
+
const mergedAssets2 = this.htmlTransformer.dedupeProcessedAssets([
|
|
291
|
+
...assets2 ?? [],
|
|
292
|
+
...queuedBoundaryResolution2.assets
|
|
293
|
+
]);
|
|
294
|
+
return {
|
|
295
|
+
html: queuedBoundaryResolution2.html,
|
|
296
|
+
canAttachAttributes: true,
|
|
297
|
+
rootTag: this.getRootTagName(queuedBoundaryResolution2.html),
|
|
298
|
+
integrationName: this.name,
|
|
299
|
+
assets: mergedAssets2.length > 0 ? mergedAssets2 : void 0
|
|
300
|
+
};
|
|
301
|
+
}
|
|
331
302
|
const componentConfig = input.component.config;
|
|
332
303
|
const context = input.integrationContext ?? {};
|
|
333
304
|
const hasResolvedChildHtml = input.children !== void 0;
|
|
334
|
-
let html = this.renderComponentHtml(input, context);
|
|
305
|
+
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
306
|
+
const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
|
|
307
|
+
html = queuedBoundaryResolution.html;
|
|
335
308
|
let canAttachAttributes = hasSingleRootElement(html);
|
|
336
309
|
let rootTag = this.getRootTagName(html);
|
|
337
310
|
const componentFile = componentConfig?.__eco?.file;
|
|
@@ -339,25 +312,40 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
339
312
|
let assets;
|
|
340
313
|
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService && !hasResolvedChildHtml) {
|
|
341
314
|
const componentInstanceId = context.componentInstanceId;
|
|
342
|
-
assets = await this.hydrationAssetService.buildComponentRenderAssets(
|
|
343
|
-
componentFile,
|
|
344
|
-
componentConfig
|
|
345
|
-
);
|
|
315
|
+
assets = await this.hydrationAssetService.buildComponentRenderAssets(componentFile, componentConfig);
|
|
346
316
|
rootAttributes = {
|
|
347
317
|
"data-eco-component-id": componentInstanceId,
|
|
348
318
|
"data-eco-component-key": getReactIslandComponentKey(componentFile, componentConfig),
|
|
349
319
|
"data-eco-props": btoa(JSON.stringify(this.buildHydrationProps(input.props)))
|
|
350
320
|
};
|
|
351
321
|
}
|
|
322
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
323
|
+
...assets ?? [],
|
|
324
|
+
...queuedBoundaryResolution.assets
|
|
325
|
+
]);
|
|
352
326
|
return {
|
|
353
327
|
html,
|
|
354
328
|
canAttachAttributes,
|
|
355
329
|
rootTag,
|
|
356
330
|
integrationName: this.name,
|
|
357
331
|
rootAttributes,
|
|
358
|
-
assets
|
|
332
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
359
333
|
};
|
|
360
334
|
}
|
|
335
|
+
createComponentBoundaryRuntime(options) {
|
|
336
|
+
return this.createQueuedBoundaryRuntime({
|
|
337
|
+
boundaryInput: options.boundaryInput,
|
|
338
|
+
rendererCache: options.rendererCache,
|
|
339
|
+
createRuntimeContext: (integrationContext, rendererCache) => ({
|
|
340
|
+
rendererCache,
|
|
341
|
+
componentInstanceScope: integrationContext.componentInstanceId,
|
|
342
|
+
nextBoundaryId: 0,
|
|
343
|
+
queuedResolutions: [],
|
|
344
|
+
rawChildrenToken: void 0,
|
|
345
|
+
rawChildrenHtml: void 0
|
|
346
|
+
})
|
|
347
|
+
});
|
|
348
|
+
}
|
|
361
349
|
/**
|
|
362
350
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
363
351
|
* @param filePath - The file path to check
|
|
@@ -543,18 +531,19 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
543
531
|
query,
|
|
544
532
|
safeLocals
|
|
545
533
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
534
|
+
return await this.renderPageWithDocumentShell({
|
|
535
|
+
page: {
|
|
536
|
+
component: Page,
|
|
537
|
+
props: { params, query, ...props, locals: pageLocals }
|
|
538
|
+
},
|
|
539
|
+
layout: Layout ? {
|
|
540
|
+
component: Layout,
|
|
541
|
+
props: locals ? { locals } : {}
|
|
542
|
+
} : void 0,
|
|
543
|
+
htmlTemplate: HtmlTemplate,
|
|
554
544
|
metadata,
|
|
555
545
|
pageProps: allPageProps,
|
|
556
|
-
|
|
557
|
-
contentHtml
|
|
546
|
+
documentProps: !this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(allPageProps) } : void 0
|
|
558
547
|
});
|
|
559
548
|
} catch (error) {
|
|
560
549
|
throw this.createRenderError("Failed to render component", error);
|
|
@@ -617,38 +606,41 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
617
606
|
const ViewComponent = this.asReactComponent(view);
|
|
618
607
|
const normalizedProps = props ?? {};
|
|
619
608
|
if (ctx.partial) {
|
|
620
|
-
|
|
621
|
-
|
|
609
|
+
return this.renderPartialViewResponse({
|
|
610
|
+
view,
|
|
611
|
+
props,
|
|
612
|
+
ctx,
|
|
613
|
+
renderInline: async () => await renderToReadableStream(createElement(ViewComponent, normalizedProps))
|
|
614
|
+
});
|
|
622
615
|
}
|
|
623
616
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
624
617
|
const metadata = await this.resolveViewMetadata(view, props);
|
|
625
618
|
await this.prepareViewDependencies(view, Layout);
|
|
626
619
|
await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
|
|
627
|
-
const
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
pageProps: normalizedProps
|
|
620
|
+
const viewRender = await this.renderComponentBoundary({
|
|
621
|
+
component: view,
|
|
622
|
+
props: normalizedProps
|
|
631
623
|
});
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
624
|
+
const layoutRender = Layout ? await this.renderComponentBoundary({
|
|
625
|
+
component: Layout,
|
|
626
|
+
props: {},
|
|
627
|
+
children: viewRender.html
|
|
628
|
+
}) : void 0;
|
|
629
|
+
const documentRender = await this.renderComponentBoundary({
|
|
630
|
+
component: HtmlTemplate,
|
|
631
|
+
props: {
|
|
632
|
+
metadata,
|
|
633
|
+
pageProps: normalizedProps,
|
|
634
|
+
...!this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(normalizedProps) } : {}
|
|
635
|
+
},
|
|
636
|
+
children: layoutRender?.html ?? viewRender.html
|
|
637
|
+
});
|
|
638
|
+
this.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
|
|
639
|
+
const transformedHtml = await this.finalizeResolvedHtml({
|
|
640
|
+
html: `${this.DOC_TYPE}${documentRender.html}`,
|
|
641
|
+
partial: false,
|
|
642
|
+
documentAttributes: this.getRouterDocumentAttributes()
|
|
638
643
|
});
|
|
639
|
-
const transformedResponse = await this.htmlTransformer.transform(
|
|
640
|
-
new Response(body, {
|
|
641
|
-
headers: { "Content-Type": "text/html" }
|
|
642
|
-
})
|
|
643
|
-
);
|
|
644
|
-
let transformedHtml = await transformedResponse.text();
|
|
645
|
-
const documentAttributes = this.getRouterDocumentAttributes();
|
|
646
|
-
if (documentAttributes) {
|
|
647
|
-
transformedHtml = this.htmlTransformer.applyAttributesToHtmlElement(
|
|
648
|
-
transformedHtml,
|
|
649
|
-
documentAttributes
|
|
650
|
-
);
|
|
651
|
-
}
|
|
652
644
|
return this.createHtmlResponse(transformedHtml, ctx);
|
|
653
645
|
} catch (error) {
|
|
654
646
|
throw this.createRenderError("Failed to render view", error);
|
package/src/react.plugin.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { CompileOptions } from '@mdx-js/mdx';
|
|
|
10
10
|
import type React from 'react';
|
|
11
11
|
import { ReactRenderer } from './react-renderer.js';
|
|
12
12
|
import type { ReactRouterAdapter } from './router-adapter.js';
|
|
13
|
-
import type { ComponentBoundaryPolicyInput } from '@ecopages/core/plugins/integration-plugin';
|
|
14
13
|
/**
|
|
15
14
|
* MDX configuration options for the React plugin
|
|
16
15
|
*/
|
|
@@ -139,17 +138,6 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
|
|
|
139
138
|
*/
|
|
140
139
|
getHmrStrategy(): HmrStrategy | undefined;
|
|
141
140
|
getRuntimeSpecifierMap(): Record<string, string>;
|
|
142
|
-
/**
|
|
143
|
-
* Declares React's boundary deferral rule for cross-integration rendering.
|
|
144
|
-
*
|
|
145
|
-
* React defers when a render pass owned by another integration enters a React
|
|
146
|
-
* component boundary. That boundary is then resolved later through the marker
|
|
147
|
-
* graph stage using the React renderer.
|
|
148
|
-
*
|
|
149
|
-
* @param input Boundary metadata for the active render pass.
|
|
150
|
-
* @returns `true` when the boundary should be deferred into the marker pass.
|
|
151
|
-
*/
|
|
152
|
-
shouldDeferComponentBoundary(input: ComponentBoundaryPolicyInput): boolean;
|
|
153
141
|
}
|
|
154
142
|
/**
|
|
155
143
|
* Factory function to create a React plugin instance
|
package/src/react.plugin.js
CHANGED
|
@@ -135,19 +135,6 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
135
135
|
getRuntimeSpecifierMap() {
|
|
136
136
|
return this.runtimeBundleService.getSpecifierMap();
|
|
137
137
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Declares React's boundary deferral rule for cross-integration rendering.
|
|
140
|
-
*
|
|
141
|
-
* React defers when a render pass owned by another integration enters a React
|
|
142
|
-
* component boundary. That boundary is then resolved later through the marker
|
|
143
|
-
* graph stage using the React renderer.
|
|
144
|
-
*
|
|
145
|
-
* @param input Boundary metadata for the active render pass.
|
|
146
|
-
* @returns `true` when the boundary should be deferred into the marker pass.
|
|
147
|
-
*/
|
|
148
|
-
shouldDeferComponentBoundary(input) {
|
|
149
|
-
return input.targetIntegration === this.name && input.currentIntegration !== this.name;
|
|
150
|
-
}
|
|
151
138
|
}
|
|
152
139
|
function reactPlugin(options) {
|
|
153
140
|
return new ReactPlugin(options);
|