@ecopages/react 0.2.0-alpha.8 → 0.2.1

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.
Files changed (43) hide show
  1. package/CHANGELOG.md +13 -11
  2. package/README.md +10 -0
  3. package/package.json +6 -6
  4. package/src/react-hmr-strategy.d.ts +4 -2
  5. package/src/react-hmr-strategy.js +36 -3
  6. package/src/react-renderer.d.ts +25 -37
  7. package/src/react-renderer.js +190 -142
  8. package/src/react.plugin.d.ts +0 -12
  9. package/src/react.plugin.js +2 -13
  10. package/src/services/react-bundle.service.d.ts +3 -1
  11. package/src/services/react-bundle.service.js +20 -2
  12. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  13. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  14. package/src/services/react-hydration-asset.service.d.ts +7 -6
  15. package/src/services/react-hydration-asset.service.js +26 -14
  16. package/src/services/react-page-module.service.js +5 -2
  17. package/src/services/react-runtime-bundle.service.d.ts +2 -0
  18. package/src/services/react-runtime-bundle.service.js +5 -0
  19. package/src/utils/client-graph-boundary-plugin.js +2 -2
  20. package/src/utils/declared-modules.js +4 -1
  21. package/src/utils/hydration-scripts.d.ts +1 -3
  22. package/src/utils/hydration-scripts.js +31 -19
  23. package/src/react-hmr-strategy.ts +0 -386
  24. package/src/react-renderer.ts +0 -803
  25. package/src/react.plugin.ts +0 -276
  26. package/src/router-adapter.ts +0 -95
  27. package/src/services/react-bundle.service.ts +0 -108
  28. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  29. package/src/services/react-hydration-asset.service.ts +0 -263
  30. package/src/services/react-page-module.service.ts +0 -224
  31. package/src/services/react-runtime-bundle.service.ts +0 -172
  32. package/src/utils/client-graph-boundary-plugin.ts +0 -831
  33. package/src/utils/client-only.ts +0 -27
  34. package/src/utils/declared-modules.ts +0 -99
  35. package/src/utils/dynamic.ts +0 -27
  36. package/src/utils/hmr-scripts.ts +0 -47
  37. package/src/utils/html-boundary.ts +0 -66
  38. package/src/utils/hydration-scripts.ts +0 -459
  39. package/src/utils/reachability-analyzer.ts +0 -593
  40. package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
  41. package/src/utils/react-mdx-loader-plugin.ts +0 -63
  42. package/src/utils/react-runtime-specifier-map.ts +0 -45
  43. package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
@@ -6,14 +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 { ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
16
+ import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
17
17
  class ReactRenderError extends Error {
18
18
  constructor(message) {
19
19
  super(message);
@@ -53,7 +53,9 @@ class ReactRenderer extends IntegrationRenderer {
53
53
  this.bundleService = new ReactBundleService({
54
54
  rootDir: this.appConfig.rootDir,
55
55
  routerAdapter: ReactRenderer.routerAdapter,
56
- mdxCompilerOptions: ReactRenderer.mdxCompilerOptions
56
+ mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
57
+ jsxImportSource: (this.appConfig.integrations ?? []).find((integration) => integration.name === this.name)?.jsxImportSource,
58
+ nonReactExtensions: (this.appConfig.integrations ?? []).filter((integration) => integration.name !== this.name).flatMap((integration) => integration.extensions)
57
59
  });
58
60
  this.pageModuleService = new ReactPageModuleService({
59
61
  rootDir: this.appConfig.rootDir,
@@ -104,7 +106,7 @@ class ReactRenderer extends IntegrationRenderer {
104
106
  *
105
107
  * React pages embedded in a non-React HTML shell still need to expose the same
106
108
  * page-data contract as fully React-owned documents so navigation and hydration
107
- * can read one marker consistently.
109
+ * can read one shared document payload consistently.
108
110
  */
109
111
  buildRouterPageDataScript(pageProps) {
110
112
  const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
@@ -163,25 +165,7 @@ class ReactRenderer extends IntegrationRenderer {
163
165
  return;
164
166
  }
165
167
  const hydrationAssets = await this.buildRouteRenderAssets(filePath);
166
- this.htmlTransformer.setProcessedDependencies([
167
- ...this.htmlTransformer.getProcessedDependencies(),
168
- ...hydrationAssets
169
- ]);
170
- }
171
- /**
172
- * Resolves metadata for direct `renderToResponse()` calls.
173
- *
174
- * View rendering bypasses the normal route-file pipeline, so metadata has to be
175
- * evaluated here from either the component-level generator or the application
176
- * default.
177
- */
178
- async resolveViewMetadata(view, props) {
179
- return view.metadata ? await view.metadata({
180
- params: {},
181
- query: {},
182
- props,
183
- appConfig: this.appConfig
184
- }) : this.appConfig.defaultMetadata;
168
+ this.appendProcessedDependencies(hydrationAssets);
185
169
  }
186
170
  /**
187
171
  * Renders a non-React layout or HTML template and enforces that mixed shells
@@ -198,64 +182,77 @@ class ReactRenderer extends IntegrationRenderer {
198
182
  throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
199
183
  }
200
184
  /**
201
- * Produces the page body before the final HTML template is applied.
185
+ * Renders one React component boundary while preserving already-resolved child HTML.
202
186
  *
203
- * This method owns the React/non-React layout split. React-managed layouts stay
204
- * as React elements so they can stream normally; non-React layouts are rendered
205
- * to HTML first and then passed through as serialized content.
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.
192
+ *
193
+ * @param input Component render input for the current boundary.
194
+ * @param context React-specific render context for stable token generation.
195
+ * @returns Serialized component HTML with resolved child markup preserved.
206
196
  */
207
- async composePageContent(options) {
208
- const pageElement = createElement(options.Page, options.pageProps);
209
- const pageHtml = renderToString(pageElement);
210
- const layoutProps = options.locals ? { locals: options.locals } : {};
211
- if (!options.Layout) {
212
- return { contentNode: pageElement, contentHtml: pageHtml };
197
+ renderComponentHtml(input, context, runtimeContext) {
198
+ if (input.children === void 0) {
199
+ return this.normalizeBoundaryArtifactHtml(
200
+ renderToString(createElement(this.asReactComponent(input.component), input.props))
201
+ );
213
202
  }
214
- if (this.isReactManagedComponent(options.Layout)) {
215
- const layoutElement = createElement(this.asReactComponent(options.Layout), layoutProps, pageElement);
216
- return {
217
- contentNode: layoutElement,
218
- contentHtml: renderToString(layoutElement)
219
- };
203
+ const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
204
+ const rawChildrenToken = `__ECO_RAW_HTML_CHILD_${context.componentInstanceId ?? "component"}__`;
205
+ if (runtimeContext) {
206
+ runtimeContext.rawChildrenToken = rawChildrenToken;
207
+ runtimeContext.rawChildrenHtml = resolvedChildHtml;
220
208
  }
221
- const layoutHtml = await this.renderNonReactShellComponent(
222
- this.asNonReactShellComponent(options.Layout),
223
- { ...layoutProps, children: pageHtml },
224
- "Layout"
209
+ const html = renderToString(
210
+ createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
225
211
  );
226
- return { contentNode: layoutHtml, contentHtml: layoutHtml };
212
+ return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
227
213
  }
228
- /**
229
- * Wraps composed page content in the final document template.
230
- *
231
- * React-owned HTML templates stream directly. Non-React templates receive
232
- * pre-rendered page HTML plus the canonical React page-data payload so the
233
- * client runtime can recover page data after cross-integration handoff.
234
- */
235
- async renderDocument(options) {
236
- if (this.isReactManagedComponent(options.HtmlTemplate)) {
237
- return renderToReadableStream(
238
- createElement(
239
- this.asReactComponent(options.HtmlTemplate),
240
- {
241
- metadata: options.metadata,
242
- pageProps: options.pageProps
243
- },
244
- options.contentNode
245
- )
246
- );
214
+ restoreRuntimeChildHtml(html, runtimeContext) {
215
+ if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
216
+ return html;
217
+ }
218
+ return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
219
+ }
220
+ async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
221
+ if (children === void 0) {
222
+ return void 0;
247
223
  }
248
- const headContent = ReactRenderer.routerAdapter ? this.buildRouterPageDataScript(options.pageProps) : void 0;
249
- return this.renderNonReactShellComponent(
250
- this.asNonReactShellComponent(options.HtmlTemplate),
251
- {
252
- metadata: options.metadata,
253
- pageProps: options.pageProps,
254
- children: options.contentHtml,
255
- headContent
256
- },
257
- "HtmlTemplate"
224
+ let html = this.normalizeBoundaryArtifactHtml(
225
+ renderToString(createElement(Fragment, null, children))
258
226
  );
227
+ html = this.restoreRuntimeChildHtml(html, runtimeContext);
228
+ html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
229
+ return html;
230
+ }
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 ?? {};
253
+ }
254
+ const { locals: _locals, ...hydrationProps } = props;
255
+ return hydrationProps;
259
256
  }
260
257
  /**
261
258
  * Renders a React component for component-level orchestration.
@@ -265,43 +262,90 @@ class ReactRenderer extends IntegrationRenderer {
265
262
  * - When an explicit component instance id is provided, a stable
266
263
  * `data-eco-component-id` attribute is attached so island hydration can target it.
267
264
  * - Without an explicit instance id, component renders remain plain SSR output.
265
+ * - When resolved child HTML is provided, that boundary is treated as a pure SSR
266
+ * composition step and does not emit hydration assets for the parent wrapper.
268
267
  *
269
268
  * This preserves DOM shape for global CSS/layout selectors while keeping a
270
269
  * deterministic mount target per component instance.
271
270
  */
272
271
  async renderComponent(input) {
273
- const Component = this.asReactComponent(input.component);
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
+ }
274
302
  const componentConfig = input.component.config;
275
- const element = input.children === void 0 ? createElement(Component, input.props) : createElement(Component, input.props, input.children);
276
- let html = renderToString(element);
303
+ const context = input.integrationContext ?? {};
304
+ const hasResolvedChildHtml = input.children !== void 0;
305
+ let html = this.renderComponentHtml(input, context, runtimeContext);
306
+ const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
307
+ html = queuedBoundaryResolution.html;
277
308
  let canAttachAttributes = hasSingleRootElement(html);
278
309
  let rootTag = this.getRootTagName(html);
279
310
  const componentFile = componentConfig?.__eco?.file;
280
- const context = input.integrationContext ?? {};
281
311
  let rootAttributes;
282
312
  let assets;
283
- if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService) {
313
+ if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService && !hasResolvedChildHtml) {
284
314
  const componentInstanceId = context.componentInstanceId;
285
- assets = await this.hydrationAssetService.buildComponentRenderAssets(
286
- componentFile,
287
- componentInstanceId,
288
- input.props,
289
- componentConfig
290
- );
315
+ assets = await this.hydrationAssetService.buildComponentRenderAssets(componentFile, componentConfig);
291
316
  rootAttributes = {
292
317
  "data-eco-component-id": componentInstanceId,
293
- "data-eco-props": btoa(JSON.stringify(input.props ?? {}))
318
+ "data-eco-component-key": getReactIslandComponentKey(componentFile, componentConfig),
319
+ "data-eco-props": btoa(JSON.stringify(this.buildHydrationProps(input.props)))
294
320
  };
295
321
  }
322
+ const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
323
+ ...assets ?? [],
324
+ ...queuedBoundaryResolution.assets
325
+ ]);
296
326
  return {
297
327
  html,
298
328
  canAttachAttributes,
299
329
  rootTag,
300
330
  integrationName: this.name,
301
331
  rootAttributes,
302
- assets
332
+ assets: mergedAssets.length > 0 ? mergedAssets : void 0
303
333
  };
304
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
+ }
305
349
  /**
306
350
  * Checks if the given file path corresponds to an MDX file based on configured extensions.
307
351
  * @param filePath - The file path to check
@@ -310,13 +354,33 @@ class ReactRenderer extends IntegrationRenderer {
310
354
  isMdxFile(filePath) {
311
355
  return this.pageModuleService.isMdxFile(filePath);
312
356
  }
357
+ usesIntegrationPageImporter(file) {
358
+ return this.pageModuleService.isMdxFile(file);
359
+ }
360
+ async importIntegrationPageFile(file) {
361
+ return await this.pageModuleService.importMdxPageFile(file);
362
+ }
363
+ normalizeImportedPageFile(file, pageModule) {
364
+ const reactModule = pageModule;
365
+ const { default: Page, getMetadata, config } = reactModule;
366
+ if (this.pageModuleService.isMdxFile(file) && config) {
367
+ Page.config = config;
368
+ }
369
+ return {
370
+ ...pageModule,
371
+ default: Page,
372
+ getMetadata,
373
+ config
374
+ };
375
+ }
313
376
  /**
314
377
  * Processes MDX-specific configuration dependencies including layout dependencies.
315
378
  * @param pagePath - Absolute path to the MDX page file
316
379
  * @returns Processed assets for MDX configuration dependencies
317
380
  */
318
381
  async processMdxConfigDependencies(pagePath) {
319
- const { config } = await this.importPageFile(pagePath);
382
+ const pageModule = await this.importPageFile(pagePath);
383
+ const config = pageModule.config;
320
384
  const resolvedLayout = config?.layout;
321
385
  const components = [];
322
386
  if (resolvedLayout?.config?.dependencies) {
@@ -439,26 +503,6 @@ class ReactRenderer extends IntegrationRenderer {
439
503
  );
440
504
  }
441
505
  }
442
- /**
443
- * Imports a page module while normalizing React MDX modules to the same shape
444
- * as ordinary React page files.
445
- *
446
- * MDX page imports can expose `config` separately from the default export. The
447
- * React renderer reattaches that config to the page component so downstream
448
- * layout, dependency, and hydration logic can treat MDX and TSX pages the same.
449
- */
450
- async importPageFile(file) {
451
- const module = this.pageModuleService.isMdxFile(file) ? await this.pageModuleService.importMdxPageFile(file) : await super.importPageFile(file);
452
- const { default: Page, getMetadata, config } = module;
453
- if (this.pageModuleService.isMdxFile(file) && config) {
454
- Page.config = config;
455
- }
456
- return {
457
- default: Page,
458
- getMetadata,
459
- config
460
- };
461
- }
462
506
  /**
463
507
  * Renders a full route response for the filesystem page pipeline.
464
508
  *
@@ -487,18 +531,19 @@ class ReactRenderer extends IntegrationRenderer {
487
531
  query,
488
532
  safeLocals
489
533
  });
490
- const { contentNode, contentHtml } = await this.composePageContent({
491
- Page: this.asReactComponent(Page),
492
- Layout,
493
- pageProps: { params, query, ...props, locals: pageLocals },
494
- locals
495
- });
496
- return await this.renderDocument({
497
- 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,
498
544
  metadata,
499
545
  pageProps: allPageProps,
500
- contentNode,
501
- contentHtml
546
+ documentProps: !this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(allPageProps) } : void 0
502
547
  });
503
548
  } catch (error) {
504
549
  throw this.createRenderError("Failed to render component", error);
@@ -561,38 +606,41 @@ class ReactRenderer extends IntegrationRenderer {
561
606
  const ViewComponent = this.asReactComponent(view);
562
607
  const normalizedProps = props ?? {};
563
608
  if (ctx.partial) {
564
- const stream = await renderToReadableStream(createElement(ViewComponent, normalizedProps));
565
- return this.createHtmlResponse(stream, ctx);
609
+ return this.renderPartialViewResponse({
610
+ view,
611
+ props,
612
+ ctx,
613
+ renderInline: async () => await renderToReadableStream(createElement(ViewComponent, normalizedProps))
614
+ });
566
615
  }
567
616
  const HtmlTemplate = await this.getHtmlTemplate();
568
617
  const metadata = await this.resolveViewMetadata(view, props);
569
618
  await this.prepareViewDependencies(view, Layout);
570
619
  await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
571
- const { contentNode, contentHtml } = await this.composePageContent({
572
- Page: ViewComponent,
573
- Layout,
574
- pageProps: normalizedProps
620
+ const viewRender = await this.renderComponentBoundary({
621
+ component: view,
622
+ props: normalizedProps
575
623
  });
576
- const body = await this.renderDocument({
577
- HtmlTemplate,
578
- metadata,
579
- pageProps: normalizedProps,
580
- contentNode,
581
- 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()
582
643
  });
583
- const transformedResponse = await this.htmlTransformer.transform(
584
- new Response(body, {
585
- headers: { "Content-Type": "text/html" }
586
- })
587
- );
588
- let transformedHtml = await transformedResponse.text();
589
- const documentAttributes = this.getRouterDocumentAttributes();
590
- if (documentAttributes) {
591
- transformedHtml = this.htmlTransformer.applyAttributesToHtmlElement(
592
- transformedHtml,
593
- documentAttributes
594
- );
595
- }
596
644
  return this.createHtmlResponse(transformedHtml, ctx);
597
645
  } catch (error) {
598
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
@@ -38,6 +38,7 @@ class ReactPlugin extends IntegrationPlugin {
38
38
  super({
39
39
  name: PLUGIN_NAME,
40
40
  extensions,
41
+ jsxImportSource: "react",
41
42
  ...restOptions
42
43
  });
43
44
  this.mdxEnabled = options?.mdx?.enabled ?? false;
@@ -70,6 +71,7 @@ class ReactPlugin extends IntegrationPlugin {
70
71
  if (this.runtimeDependenciesInitialized) {
71
72
  return;
72
73
  }
74
+ this.runtimeBundleService.setRootDir(this.appConfig?.rootDir);
73
75
  this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
74
76
  this.runtimeDependenciesInitialized = true;
75
77
  }
@@ -133,19 +135,6 @@ class ReactPlugin extends IntegrationPlugin {
133
135
  getRuntimeSpecifierMap() {
134
136
  return this.runtimeBundleService.getSpecifierMap();
135
137
  }
136
- /**
137
- * Declares React's boundary deferral rule for cross-integration rendering.
138
- *
139
- * React defers when a render pass owned by another integration enters a React
140
- * component boundary. That boundary is then resolved later through the marker
141
- * graph stage using the React renderer.
142
- *
143
- * @param input Boundary metadata for the active render pass.
144
- * @returns `true` when the boundary should be deferred into the marker pass.
145
- */
146
- shouldDeferComponentBoundary(input) {
147
- return input.targetIntegration === this.name && input.currentIntegration !== this.name;
148
- }
149
138
  }
150
139
  function reactPlugin(options) {
151
140
  return new ReactPlugin(options);
@@ -16,6 +16,8 @@ export interface ReactBundleServiceConfig {
16
16
  rootDir: string;
17
17
  routerAdapter?: ReactRouterAdapter;
18
18
  mdxCompilerOptions?: CompileOptions;
19
+ nonReactExtensions?: string[];
20
+ jsxImportSource?: string;
19
21
  }
20
22
  /**
21
23
  * Manages esbuild bundle configuration and plugin creation for React page/component builds.
@@ -41,5 +43,5 @@ export declare class ReactBundleService {
41
43
  * Creates the esbuild plugin that rewrites bare React specifiers
42
44
  * to their runtime asset URLs.
43
45
  */
44
- createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("packages/core/src/build/build-types.ts").EcoBuildPlugin | null;
46
+ createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("@ecopages/core/build/build-types").EcoBuildPlugin | null;
45
47
  }
@@ -6,6 +6,7 @@ import {
6
6
  } from "../utils/react-runtime-specifier-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
+ import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
9
10
  import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
10
11
  class ReactBundleService {
11
12
  runtimeBundleService;
@@ -13,6 +14,7 @@ class ReactBundleService {
13
14
  constructor(config) {
14
15
  this.config = config;
15
16
  this.runtimeBundleService = new ReactRuntimeBundleService({
17
+ rootDir: config.rootDir,
16
18
  routerAdapter: config.routerAdapter
17
19
  });
18
20
  }
@@ -48,6 +50,11 @@ class ReactBundleService {
48
50
  declaredModules,
49
51
  alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
50
52
  });
53
+ const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
54
+ name: "react-renderer-foreign-jsx-override",
55
+ hostJsxImportSource: this.config.jsxImportSource ?? "react",
56
+ foreignExtensions: this.config.nonReactExtensions ?? []
57
+ });
51
58
  const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
52
59
  const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
53
60
  name: "react-renderer-use-sync-external-store-shim",
@@ -56,9 +63,20 @@ class ReactBundleService {
56
63
  if (isMdx && this.config.mdxCompilerOptions) {
57
64
  const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
58
65
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
59
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin];
66
+ options.plugins = [
67
+ foreignJsxOverridePlugin,
68
+ graphBoundaryPlugin,
69
+ runtimeAliasPlugin,
70
+ mdxPlugin,
71
+ useSyncExternalStoreShimPlugin
72
+ ];
60
73
  } else {
61
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, useSyncExternalStoreShimPlugin];
74
+ options.plugins = [
75
+ foreignJsxOverridePlugin,
76
+ graphBoundaryPlugin,
77
+ runtimeAliasPlugin,
78
+ useSyncExternalStoreShimPlugin
79
+ ];
62
80
  }
63
81
  return options;
64
82
  }
@@ -6,6 +6,11 @@
6
6
  */
7
7
  export declare class ReactHmrPageMetadataCache {
8
8
  private readonly declaredModulesByEntrypoint;
9
+ private readonly ownedEntrypoints;
10
+ /**
11
+ * Marks an HMR entrypoint as React-owned.
12
+ */
13
+ markOwnedEntrypoint(entrypointPath: string): void;
9
14
  /**
10
15
  * Stores the declared browser modules for a page entrypoint.
11
16
  */
@@ -14,4 +19,8 @@ export declare class ReactHmrPageMetadataCache {
14
19
  * Returns the last known declared browser modules for a page entrypoint.
15
20
  */
16
21
  getDeclaredModules(entrypointPath: string): string[] | undefined;
22
+ /**
23
+ * Returns true when the watched entrypoint is owned by the React integration.
24
+ */
25
+ ownsEntrypoint(entrypointPath: string): boolean;
17
26
  }
@@ -1,18 +1,34 @@
1
+ import path from "node:path";
1
2
  class ReactHmrPageMetadataCache {
2
3
  declaredModulesByEntrypoint = /* @__PURE__ */ new Map();
4
+ ownedEntrypoints = /* @__PURE__ */ new Set();
5
+ /**
6
+ * Marks an HMR entrypoint as React-owned.
7
+ */
8
+ markOwnedEntrypoint(entrypointPath) {
9
+ this.ownedEntrypoints.add(path.resolve(entrypointPath));
10
+ }
3
11
  /**
4
12
  * Stores the declared browser modules for a page entrypoint.
5
13
  */
6
14
  setDeclaredModules(entrypointPath, declaredModules) {
7
- this.declaredModulesByEntrypoint.set(entrypointPath, [...declaredModules]);
15
+ const resolvedEntrypointPath = path.resolve(entrypointPath);
16
+ this.markOwnedEntrypoint(resolvedEntrypointPath);
17
+ this.declaredModulesByEntrypoint.set(resolvedEntrypointPath, [...declaredModules]);
8
18
  }
9
19
  /**
10
20
  * Returns the last known declared browser modules for a page entrypoint.
11
21
  */
12
22
  getDeclaredModules(entrypointPath) {
13
- const declaredModules = this.declaredModulesByEntrypoint.get(entrypointPath);
23
+ const declaredModules = this.declaredModulesByEntrypoint.get(path.resolve(entrypointPath));
14
24
  return declaredModules ? [...declaredModules] : void 0;
15
25
  }
26
+ /**
27
+ * Returns true when the watched entrypoint is owned by the React integration.
28
+ */
29
+ ownsEntrypoint(entrypointPath) {
30
+ return this.ownedEntrypoints.has(path.resolve(entrypointPath));
31
+ }
16
32
  }
17
33
  export {
18
34
  ReactHmrPageMetadataCache