@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 CHANGED
@@ -8,11 +8,7 @@ All notable changes to `@ecopages/react` are documented here.
8
8
 
9
9
  ### Bug Fixes
10
10
 
11
- - Fixed development hydration, router HMR ownership, and page bootstraps across Bun, Vite, and Nitro flows.
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.12",
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.12",
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.12",
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",
@@ -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 marker consistently.
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 for marker-graph orchestration.
123
+ * Renders one React component boundary while preserving already-resolved child HTML.
132
124
  *
133
- * When the marker resolver has already resolved child HTML for this boundary,
134
- * the child payload must remain raw SSR output rather than a React string
135
- * child, otherwise React would escape it. This helper renders a unique token
136
- * through React and swaps that token back to the resolved HTML afterward.
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 reconstructed from marker metadata.
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
@@ -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("&quot;", '"').replaceAll("&#39;", "'").replaceAll("&#x27;", "'").replaceAll("&lt;", "<").replaceAll("&gt;", ">").replaceAll("&amp;", "&");
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 marker consistently.
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.htmlTransformer.setProcessedDependencies([
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 for marker-graph orchestration.
185
+ * Renders one React component boundary while preserving already-resolved child HTML.
222
186
  *
223
- * When the marker resolver has already resolved child HTML for this boundary,
224
- * the child payload must remain raw SSR output rather than a React string
225
- * child, otherwise React would escape it. This helper renders a unique token
226
- * through React and swaps that token back to the resolved HTML afterward.
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 reconstructed from marker metadata.
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 restoreEscapedComponentMarkers(
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 restoreEscapedComponentMarkers(html.split(rawChildrenToken).join(resolvedChildHtml));
212
+ return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
244
213
  }
245
- buildHydrationProps(props) {
246
- if (!props || !Object.prototype.hasOwnProperty.call(props, "locals")) {
247
- return props ?? {};
214
+ restoreRuntimeChildHtml(html, runtimeContext) {
215
+ if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
216
+ return html;
248
217
  }
249
- const { locals: _locals, ...hydrationProps } = props;
250
- return hydrationProps;
218
+ return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
251
219
  }
252
- /**
253
- * Produces the page body before the final HTML template is applied.
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
- const layoutHtml = await this.renderNonReactShellComponent(
274
- this.asNonReactShellComponent(options.Layout),
275
- { ...layoutProps, children: pageHtml },
276
- "Layout"
224
+ let html = this.normalizeBoundaryArtifactHtml(
225
+ renderToString(createElement(Fragment, null, children))
277
226
  );
278
- return { contentNode: layoutHtml, contentHtml: layoutHtml };
227
+ html = this.restoreRuntimeChildHtml(html, runtimeContext);
228
+ html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
229
+ return html;
279
230
  }
280
- /**
281
- * Wraps composed page content in the final document template.
282
- *
283
- * React-owned HTML templates stream directly. Non-React templates receive
284
- * pre-rendered page HTML plus the canonical React page-data payload so the
285
- * client runtime can recover page data after cross-integration handoff.
286
- */
287
- async renderDocument(options) {
288
- if (this.isReactManagedComponent(options.HtmlTemplate)) {
289
- const rawChildrenToken = "__ECO_RAW_HTML_DOCUMENT_CHILD__";
290
- const html = restoreEscapedComponentMarkers(
291
- renderToString(
292
- createElement(
293
- this.asReactComponent(options.HtmlTemplate),
294
- {
295
- metadata: options.metadata,
296
- pageProps: options.pageProps
297
- },
298
- rawChildrenToken
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 headContent = ReactRenderer.routerAdapter ? this.buildRouterPageDataScript(options.pageProps) : void 0;
305
- return this.DOC_TYPE + await this.renderNonReactShellComponent(
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
- const { contentNode, contentHtml } = await this.composePageContent({
547
- Page: this.asReactComponent(Page),
548
- Layout,
549
- pageProps: { params, query, ...props, locals: pageLocals },
550
- locals
551
- });
552
- return await this.renderDocument({
553
- HtmlTemplate,
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
- contentNode,
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
- const stream = await renderToReadableStream(createElement(ViewComponent, normalizedProps));
621
- return this.createHtmlResponse(stream, ctx);
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 { contentNode, contentHtml } = await this.composePageContent({
628
- Page: ViewComponent,
629
- Layout,
630
- pageProps: normalizedProps
620
+ const viewRender = await this.renderComponentBoundary({
621
+ component: view,
622
+ props: normalizedProps
631
623
  });
632
- const body = await this.renderDocument({
633
- HtmlTemplate,
634
- metadata,
635
- pageProps: normalizedProps,
636
- contentNode,
637
- contentHtml
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);
@@ -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
@@ -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);