@ecopages/react 0.2.0-alpha.13 → 0.2.0-alpha.15

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.
@@ -1,18 +1,18 @@
1
- import { IntegrationRenderer } from "@ecopages/core/route-renderer/integration-renderer";
2
- import { LocalsAccessError } from "@ecopages/core/errors/locals-access-error";
1
+ import {
2
+ IntegrationRenderer
3
+ } from "@ecopages/core/route-renderer/integration-renderer";
3
4
  import { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
4
5
  import { getAppBuildExecutor } from "@ecopages/core/build/build-adapter";
5
- import { rapidhash } from "@ecopages/core/hash";
6
- import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
7
6
  import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
7
+ import { createRequire } from "node:module";
8
8
  import path from "node:path";
9
- import { createElement, Fragment } from "react";
10
- import { renderToReadableStream, renderToString } from "react-dom/server";
11
9
  import { PLUGIN_NAME } from "./react.plugin.js";
12
10
  import { hasSingleRootElement } from "./utils/html-boundary.js";
13
11
  import { ReactBundleService } from "./services/react-bundle.service.js";
14
12
  import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
13
+ import { ReactMdxConfigDependencyService } from "./services/react-mdx-config-dependency.service.js";
15
14
  import { ReactPageModuleService } from "./services/react-page-module.service.js";
15
+ import { ReactPagePayloadService } from "./services/react-page-payload.service.js";
16
16
  import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
17
17
  class ReactRenderError extends Error {
18
18
  constructor(message) {
@@ -31,29 +31,40 @@ class BundleError extends Error {
31
31
  class ReactRenderer extends IntegrationRenderer {
32
32
  name = PLUGIN_NAME;
33
33
  componentDirectory = RESOLVED_ASSETS_DIR;
34
- static routerAdapter;
35
- static mdxCompilerOptions;
36
- static mdxExtensions = [".mdx"];
37
- static hmrPageMetadataCache;
34
+ reactRuntimeModules;
35
+ routerAdapter;
36
+ mdxCompilerOptions;
37
+ mdxExtensions;
38
+ hmrPageMetadataCache;
38
39
  /**
39
40
  * Enables explicit graph behavior for React page-entry bundling.
40
41
  *
41
42
  * When true, page-entry bundles disable AST server-only stripping and rely
42
43
  * on explicit dependency declarations for browser graph composition.
43
44
  */
44
- static explicitGraphEnabled = false;
45
+ explicitGraphEnabled;
45
46
  /** @internal */
46
47
  bundleService;
47
48
  /** @internal */
48
49
  pageModuleService;
49
50
  /** @internal */
50
51
  hydrationAssetService;
52
+ /** @internal */
53
+ pagePayloadService;
54
+ /** @internal */
55
+ mdxConfigDependencyService;
51
56
  constructor(options) {
52
- super(options);
57
+ const { reactConfig, ...rendererOptions } = options;
58
+ super(rendererOptions);
59
+ this.routerAdapter = reactConfig?.routerAdapter;
60
+ this.mdxCompilerOptions = reactConfig?.mdxCompilerOptions;
61
+ this.mdxExtensions = reactConfig?.mdxExtensions ?? [".mdx"];
62
+ this.hmrPageMetadataCache = reactConfig?.hmrPageMetadataCache;
63
+ this.explicitGraphEnabled = reactConfig?.explicitGraphEnabled ?? false;
53
64
  this.bundleService = new ReactBundleService({
54
65
  rootDir: this.appConfig.rootDir,
55
- routerAdapter: ReactRenderer.routerAdapter,
56
- mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
66
+ routerAdapter: this.routerAdapter,
67
+ mdxCompilerOptions: this.mdxCompilerOptions,
57
68
  jsxImportSource: (this.appConfig.integrations ?? []).find((integration) => integration.name === this.name)?.jsxImportSource,
58
69
  nonReactExtensions: (this.appConfig.integrations ?? []).filter((integration) => integration.name !== this.name).flatMap((integration) => integration.extensions)
59
70
  });
@@ -64,17 +75,23 @@ class ReactRenderer extends IntegrationRenderer {
64
75
  buildExecutor: getAppBuildExecutor(this.appConfig),
65
76
  layoutsDir: this.appConfig.absolutePaths.layoutsDir,
66
77
  componentsDir: this.appConfig.absolutePaths.componentsDir,
67
- mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
68
- mdxExtensions: ReactRenderer.mdxExtensions,
78
+ mdxCompilerOptions: this.mdxCompilerOptions,
79
+ mdxExtensions: this.mdxExtensions,
69
80
  integrationName: this.name,
70
- hasRouterAdapter: Boolean(ReactRenderer.routerAdapter)
81
+ hasRouterAdapter: Boolean(this.routerAdapter)
71
82
  });
72
83
  this.hydrationAssetService = new ReactHydrationAssetService({
73
84
  srcDir: this.appConfig.srcDir,
74
- routerAdapter: ReactRenderer.routerAdapter,
85
+ routerAdapter: this.routerAdapter,
75
86
  assetProcessingService: this.assetProcessingService,
76
87
  bundleService: this.bundleService,
77
- hmrPageMetadataCache: ReactRenderer.hmrPageMetadataCache
88
+ hmrPageMetadataCache: this.hmrPageMetadataCache
89
+ });
90
+ this.pagePayloadService = new ReactPagePayloadService();
91
+ this.mdxConfigDependencyService = new ReactMdxConfigDependencyService({
92
+ integrationName: this.name,
93
+ pageModuleService: this.pageModuleService,
94
+ assetProcessingService: this.assetProcessingService
78
95
  });
79
96
  }
80
97
  shouldRenderPageComponent() {
@@ -101,19 +118,8 @@ class ReactRenderer extends IntegrationRenderer {
101
118
  const integration = this.getComponentIntegration(component);
102
119
  return integration === void 0 || integration === this.name;
103
120
  }
104
- /**
105
- * Creates the canonical page-props payload used by router hydration.
106
- *
107
- * React pages embedded in a non-React HTML shell still need to expose the same
108
- * page-data contract as fully React-owned documents so navigation and hydration
109
- * can read one shared document payload consistently.
110
- */
111
- buildRouterPageDataScript(pageProps) {
112
- const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
113
- return `<script id="__ECO_PAGE_DATA__" type="application/json">${safeJson}<\/script>`;
114
- }
115
121
  getRouterDocumentAttributes() {
116
- if (!ReactRenderer.routerAdapter) {
122
+ if (!this.routerAdapter) {
117
123
  return void 0;
118
124
  }
119
125
  return {
@@ -141,20 +147,25 @@ class ReactRenderer extends IntegrationRenderer {
141
147
  asNonReactShellComponent(component) {
142
148
  return component;
143
149
  }
144
- /**
145
- * Builds the serialized page-props payload embedded into the final HTML.
146
- *
147
- * The document payload is intentionally narrower than the full server render
148
- * input: only routing data, public page props, and explicitly allowed locals are
149
- * exposed to the browser.
150
- */
151
- buildSerializedPageProps(options) {
152
- return {
153
- ...options.pageProps,
154
- params: options.params,
155
- query: options.query,
156
- ...options.safeLocals && { locals: options.safeLocals }
157
- };
150
+ resolveReactRuntimeModules() {
151
+ const appPackageJsonPath = path.resolve(this.appConfig.rootDir || process.cwd(), "package.json");
152
+ try {
153
+ const requireFromApp = createRequire(appPackageJsonPath);
154
+ return {
155
+ react: requireFromApp("react"),
156
+ reactDomServer: requireFromApp("react-dom/server")
157
+ };
158
+ } catch {
159
+ const requireFromIntegration = createRequire(import.meta.url);
160
+ return {
161
+ react: requireFromIntegration("react"),
162
+ reactDomServer: requireFromIntegration("react-dom/server")
163
+ };
164
+ }
165
+ }
166
+ getReactRuntimeModules() {
167
+ this.reactRuntimeModules ??= this.resolveReactRuntimeModules();
168
+ return this.reactRuntimeModules;
158
169
  }
159
170
  /**
160
171
  * Appends route hydration assets for a concrete page/view file to the current
@@ -195,9 +206,10 @@ class ReactRenderer extends IntegrationRenderer {
195
206
  * @returns Serialized component HTML with resolved child markup preserved.
196
207
  */
197
208
  renderComponentHtml(input, context, runtimeContext) {
209
+ const { react, reactDomServer } = this.getReactRuntimeModules();
198
210
  if (input.children === void 0) {
199
211
  return this.normalizeBoundaryArtifactHtml(
200
- renderToString(createElement(this.asReactComponent(input.component), input.props))
212
+ reactDomServer.renderToString(react.createElement(this.asReactComponent(input.component), input.props))
201
213
  );
202
214
  }
203
215
  const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
@@ -206,28 +218,54 @@ class ReactRenderer extends IntegrationRenderer {
206
218
  runtimeContext.rawChildrenToken = rawChildrenToken;
207
219
  runtimeContext.rawChildrenHtml = resolvedChildHtml;
208
220
  }
209
- const html = renderToString(
210
- createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
221
+ const html = reactDomServer.renderToString(
222
+ react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
211
223
  );
212
224
  return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
213
225
  }
226
+ /**
227
+ * Restores raw child HTML that was temporarily replaced by a token during React SSR.
228
+ *
229
+ * Queued boundary resolution may render children through a fragment path before all
230
+ * nested integration tokens are resolved. When that happens, React must never see
231
+ * the resolved child HTML as a normal string child or it would escape it. The
232
+ * runtime context stores the placeholder token and the raw child HTML so the
233
+ * fragment render path can reinsert it before foreign boundary tokens are handled.
234
+ */
214
235
  restoreRuntimeChildHtml(html, runtimeContext) {
215
236
  if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
216
237
  return html;
217
238
  }
218
239
  return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
219
240
  }
241
+ /**
242
+ * Renders queued child content through React and then resolves nested boundary tokens.
243
+ *
244
+ * This path is only used for children that were deferred while React rendered the
245
+ * parent boundary. It first restores any raw child HTML placeholders owned by the
246
+ * current runtime context, then asks the shared queued-boundary resolver to swap
247
+ * foreign integration tokens with their resolved HTML.
248
+ */
220
249
  async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
221
250
  if (children === void 0) {
222
251
  return void 0;
223
252
  }
253
+ const { react, reactDomServer } = this.getReactRuntimeModules();
224
254
  let html = this.normalizeBoundaryArtifactHtml(
225
- renderToString(createElement(Fragment, null, children))
255
+ reactDomServer.renderToString(react.createElement(react.Fragment, null, children))
226
256
  );
227
257
  html = this.restoreRuntimeChildHtml(html, runtimeContext);
228
258
  html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
229
259
  return html;
230
260
  }
261
+ /**
262
+ * Resolves queued renderer-owned boundary tokens produced during React component rendering.
263
+ *
264
+ * React components can enqueue nested boundaries while the parent HTML is being
265
+ * rendered. This delegates to the shared renderer-owned queue resolver but keeps
266
+ * the React-specific child rendering behavior local so raw child HTML and React's
267
+ * fragment rendering semantics stay coordinated.
268
+ */
231
269
  async resolveQueuedBoundaryHtml(html, runtimeContext) {
232
270
  return this.resolveRendererOwnedQueuedBoundaryHtml({
233
271
  html,
@@ -255,58 +293,71 @@ class ReactRenderer extends IntegrationRenderer {
255
293
  return hydrationProps;
256
294
  }
257
295
  /**
258
- * Renders a React component for component-level orchestration.
296
+ * Builds the extra document props needed when React renders through a non-React HTML shell.
259
297
  *
260
- * Behavior:
261
- * - SSR always returns the component's own root HTML (no synthetic wrapper).
262
- * - When an explicit component instance id is provided, a stable
263
- * `data-eco-component-id` attribute is attached so island hydration can target it.
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.
298
+ * Router-backed React pages still need to publish the canonical page-data script
299
+ * even when the outer document shell belongs to another integration.
300
+ */
301
+ buildNonReactDocumentProps(htmlTemplate, pageProps) {
302
+ if (this.isReactManagedComponent(htmlTemplate) || !this.routerAdapter) {
303
+ return void 0;
304
+ }
305
+ return {
306
+ headContent: this.pagePayloadService.buildRouterPageDataScript(pageProps)
307
+ };
308
+ }
309
+ /**
310
+ * Renders a foreign integration component boundary that participates in React composition.
267
311
  *
268
- * This preserves DOM shape for global CSS/layout selectors while keeping a
269
- * deterministic mount target per component instance.
312
+ * Non-React components must resolve to serialized HTML so React can embed them as
313
+ * mixed-shell boundaries. Any component-owned dependencies still need to flow
314
+ * through the shared dependency resolver before queued boundary tokens are finalized.
270
315
  */
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
316
+ async renderForeignComponentBoundary(input, runtimeContext) {
317
+ let props = input.props;
318
+ if (input.children !== void 0) {
319
+ props = {
320
+ ...input.props,
321
+ children: typeof input.children === "string" ? input.children : String(input.children ?? "")
300
322
  };
301
323
  }
324
+ const html = await this.renderNonReactShellComponent(
325
+ this.asNonReactShellComponent(input.component),
326
+ props,
327
+ "Component"
328
+ );
329
+ const hasDependencies = Boolean(input.component.config?.dependencies);
330
+ const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
331
+ const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
332
+ const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
333
+ const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
334
+ ...assets ?? [],
335
+ ...queuedBoundaryResolution.assets
336
+ ]);
337
+ return {
338
+ html: queuedBoundaryResolution.html,
339
+ canAttachAttributes: true,
340
+ rootTag: this.getRootTagName(queuedBoundaryResolution.html),
341
+ integrationName: this.name,
342
+ assets: mergedAssets.length > 0 ? mergedAssets : void 0
343
+ };
344
+ }
345
+ /**
346
+ * Renders a React-owned component boundary and attaches island hydration metadata when possible.
347
+ *
348
+ * This path keeps React-owned SSR, queued boundary resolution, and optional
349
+ * island hydration wiring together so the public `renderComponent()` method can
350
+ * read as orchestration rather than implementation detail.
351
+ */
352
+ async renderReactComponentBoundary(input, runtimeContext) {
302
353
  const componentConfig = input.component.config;
303
354
  const context = input.integrationContext ?? {};
304
355
  const hasResolvedChildHtml = input.children !== void 0;
305
356
  let html = this.renderComponentHtml(input, context, runtimeContext);
306
357
  const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
307
358
  html = queuedBoundaryResolution.html;
308
- let canAttachAttributes = hasSingleRootElement(html);
309
- let rootTag = this.getRootTagName(html);
359
+ const canAttachAttributes = hasSingleRootElement(html);
360
+ const rootTag = this.getRootTagName(html);
310
361
  const componentFile = componentConfig?.__eco?.file;
311
362
  let rootAttributes;
312
363
  let assets;
@@ -332,6 +383,27 @@ class ReactRenderer extends IntegrationRenderer {
332
383
  assets: mergedAssets.length > 0 ? mergedAssets : void 0
333
384
  };
334
385
  }
386
+ /**
387
+ * Renders a React component for component-level orchestration.
388
+ *
389
+ * Behavior:
390
+ * - SSR always returns the component's own root HTML (no synthetic wrapper).
391
+ * - When an explicit component instance id is provided, a stable
392
+ * `data-eco-component-id` attribute is attached so island hydration can target it.
393
+ * - Without an explicit instance id, component renders remain plain SSR output.
394
+ * - When resolved child HTML is provided, that boundary is treated as a pure SSR
395
+ * composition step and does not emit hydration assets for the parent wrapper.
396
+ *
397
+ * This preserves DOM shape for global CSS/layout selectors while keeping a
398
+ * deterministic mount target per component instance.
399
+ */
400
+ async renderComponent(input) {
401
+ const runtimeContext = this.getQueuedBoundaryRuntime(input);
402
+ if (!this.isReactManagedComponent(input.component)) {
403
+ return this.renderForeignComponentBoundary(input, runtimeContext);
404
+ }
405
+ return this.renderReactComponentBoundary(input, runtimeContext);
406
+ }
335
407
  createComponentBoundaryRuntime(options) {
336
408
  return this.createQueuedBoundaryRuntime({
337
409
  boundaryInput: options.boundaryInput,
@@ -357,8 +429,8 @@ class ReactRenderer extends IntegrationRenderer {
357
429
  usesIntegrationPageImporter(file) {
358
430
  return this.pageModuleService.isMdxFile(file);
359
431
  }
360
- async importIntegrationPageFile(file) {
361
- return await this.pageModuleService.importMdxPageFile(file);
432
+ async importIntegrationPageFile(file, options) {
433
+ return await this.pageModuleService.importMdxPageFile(file, options);
362
434
  }
363
435
  normalizeImportedPageFile(file, pageModule) {
364
436
  const reactModule = pageModule;
@@ -373,112 +445,10 @@ class ReactRenderer extends IntegrationRenderer {
373
445
  config
374
446
  };
375
447
  }
376
- /**
377
- * Processes MDX-specific configuration dependencies including layout dependencies.
378
- * @param pagePath - Absolute path to the MDX page file
379
- * @returns Processed assets for MDX configuration dependencies
380
- */
381
- async processMdxConfigDependencies(pagePath) {
382
- const pageModule = await this.importPageFile(pagePath);
383
- const config = pageModule.config;
384
- const resolvedLayout = config?.layout;
385
- const components = [];
386
- if (resolvedLayout?.config?.dependencies) {
387
- const layoutConfig = this.pageModuleService.ensureConfigFileMetadata(resolvedLayout.config, pagePath);
388
- components.push({ config: layoutConfig });
389
- }
390
- if (config?.dependencies) {
391
- const configWithMeta = {
392
- ...config,
393
- __eco: { id: rapidhash(pagePath).toString(36), file: pagePath, integration: "react" }
394
- };
395
- components.push({ config: configWithMeta });
396
- }
397
- const processedDependencies = await this.processComponentDependencies(components);
398
- const eagerSsrLazyDependencies = await this.processDeclaredMdxSsrLazyDependencies(components, pagePath);
399
- return [...processedDependencies, ...eagerSsrLazyDependencies];
400
- }
401
- async processDeclaredMdxSsrLazyDependencies(components, pagePath) {
402
- if (!this.assetProcessingService?.processDependencies) {
403
- return [];
404
- }
405
- const dependencies = this.collectDeclaredMdxSsrLazyDependencies(components);
406
- if (dependencies.length === 0) {
407
- return [];
408
- }
409
- return this.assetProcessingService.processDependencies(dependencies, `react-mdx-ssr-lazy:${pagePath}`);
410
- }
411
- collectDeclaredMdxSsrLazyDependencies(components) {
412
- const dependencies = [];
413
- const visitedConfigs = /* @__PURE__ */ new Set();
414
- const seenKeys = /* @__PURE__ */ new Set();
415
- const normalizeAttributes = (attributes) => ({
416
- type: "module",
417
- defer: "",
418
- ...attributes ?? {}
419
- });
420
- const collect = (config) => {
421
- if (!config || visitedConfigs.has(config)) {
422
- return;
423
- }
424
- visitedConfigs.add(config);
425
- const componentFile = config.__eco?.file;
426
- if (componentFile) {
427
- const componentDir = path.dirname(componentFile);
428
- for (const script of config.dependencies?.scripts ?? []) {
429
- if (typeof script === "string" || !script.lazy || script.ssr !== true) {
430
- continue;
431
- }
432
- const attributes = normalizeAttributes(script.attributes);
433
- if (script.content) {
434
- const key2 = `content:${script.content}:${JSON.stringify(attributes)}`;
435
- if (seenKeys.has(key2)) {
436
- continue;
437
- }
438
- seenKeys.add(key2);
439
- dependencies.push(
440
- AssetFactory.createContentScript({
441
- position: "head",
442
- content: script.content,
443
- attributes
444
- })
445
- );
446
- continue;
447
- }
448
- if (!script.src) {
449
- continue;
450
- }
451
- const resolvedPath = path.resolve(componentDir, script.src);
452
- const key = `file:${resolvedPath}:${JSON.stringify(attributes)}`;
453
- if (seenKeys.has(key)) {
454
- continue;
455
- }
456
- seenKeys.add(key);
457
- dependencies.push(
458
- AssetFactory.createFileScript({
459
- filepath: resolvedPath,
460
- position: "head",
461
- attributes
462
- })
463
- );
464
- }
465
- }
466
- if (config.layout?.config) {
467
- collect(config.layout.config);
468
- }
469
- for (const nestedComponent of config.dependencies?.components ?? []) {
470
- collect(nestedComponent?.config);
471
- }
472
- };
473
- for (const component of components) {
474
- collect(component.config);
475
- }
476
- return dependencies;
477
- }
478
448
  async buildRouteRenderAssets(pagePath) {
479
449
  try {
480
450
  const pageModule = await this.importPageFile(pagePath);
481
- const shouldHydrate = ReactRenderer.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
451
+ const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
482
452
  if (!shouldHydrate) {
483
453
  return [];
484
454
  }
@@ -490,7 +460,11 @@ class ReactRenderer extends IntegrationRenderer {
490
460
  declaredModules
491
461
  );
492
462
  if (isMdx) {
493
- const mdxConfigAssets = await this.processMdxConfigDependencies(pagePath);
463
+ const mdxConfigAssets = await this.mdxConfigDependencyService.processMdxConfigDependencies({
464
+ pagePath,
465
+ config: pageModule.config,
466
+ processComponentDependencies: async (components) => await this.processComponentDependencies(components)
467
+ });
494
468
  return [...processedAssets, ...mdxConfigAssets];
495
469
  }
496
470
  return processedAssets;
@@ -524,8 +498,11 @@ class ReactRenderer extends IntegrationRenderer {
524
498
  pageProps
525
499
  }) {
526
500
  try {
527
- const safeLocals = this.getSerializableLocals(locals, Page.requires);
528
- const allPageProps = this.buildSerializedPageProps({
501
+ const safeLocals = this.pagePayloadService.getSerializableLocals(
502
+ locals,
503
+ Page.requires
504
+ );
505
+ const allPageProps = this.pagePayloadService.buildSerializedPageProps({
529
506
  pageProps,
530
507
  params,
531
508
  query,
@@ -543,7 +520,7 @@ class ReactRenderer extends IntegrationRenderer {
543
520
  htmlTemplate: HtmlTemplate,
544
521
  metadata,
545
522
  pageProps: allPageProps,
546
- documentProps: !this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(allPageProps) } : void 0
523
+ documentProps: this.buildNonReactDocumentProps(HtmlTemplate, allPageProps)
547
524
  });
548
525
  } catch (error) {
549
526
  throw this.createRenderError("Failed to render component", error);
@@ -552,45 +529,6 @@ class ReactRenderer extends IntegrationRenderer {
552
529
  getDocumentAttributes() {
553
530
  return this.getRouterDocumentAttributes();
554
531
  }
555
- /**
556
- * Safely extracts the declared subset of locals for client-side hydration.
557
- *
558
- * On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
559
- * request-scoped data (e.g., session). Only keys explicitly declared via
560
- * `Page.requires` are serialized to the client so sensitive request-only data
561
- * is not leaked into hydration payloads by default.
562
- *
563
- * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
564
- * to prevent accidental use. This method safely detects that case and returns
565
- * `undefined` instead of throwing.
566
- *
567
- * @param locals - The locals object from the render context
568
- * @param requiredLocals - Keys explicitly requested for client hydration
569
- * @returns The filtered locals object if serializable, undefined otherwise
570
- */
571
- getSerializableLocals(locals, requiredLocals) {
572
- try {
573
- if (!locals) {
574
- return void 0;
575
- }
576
- const requiredKeys = requiredLocals ? Array.isArray(requiredLocals) ? requiredLocals : [requiredLocals] : [];
577
- if (requiredKeys.length === 0) {
578
- return void 0;
579
- }
580
- const serializedLocals = Object.fromEntries(
581
- requiredKeys.filter((key) => Object.prototype.hasOwnProperty.call(locals, key)).map((key) => [key, locals[key]])
582
- );
583
- if (Object.keys(serializedLocals).length > 0) {
584
- return serializedLocals;
585
- }
586
- return void 0;
587
- } catch (e) {
588
- if (e instanceof LocalsAccessError) {
589
- return void 0;
590
- }
591
- throw e;
592
- }
593
- }
594
532
  /**
595
533
  * Renders an arbitrary React view through the application's HTML shell.
596
534
  *
@@ -601,6 +539,7 @@ class ReactRenderer extends IntegrationRenderer {
601
539
  */
602
540
  async renderToResponse(view, props, ctx) {
603
541
  try {
542
+ const { react, reactDomServer } = this.getReactRuntimeModules();
604
543
  const viewConfig = view.config;
605
544
  const Layout = viewConfig?.layout;
606
545
  const ViewComponent = this.asReactComponent(view);
@@ -610,7 +549,9 @@ class ReactRenderer extends IntegrationRenderer {
610
549
  view,
611
550
  props,
612
551
  ctx,
613
- renderInline: async () => await renderToReadableStream(createElement(ViewComponent, normalizedProps))
552
+ renderInline: async () => await reactDomServer.renderToReadableStream(
553
+ react.createElement(ViewComponent, normalizedProps)
554
+ )
614
555
  });
615
556
  }
616
557
  const HtmlTemplate = await this.getHtmlTemplate();
@@ -631,7 +572,7 @@ class ReactRenderer extends IntegrationRenderer {
631
572
  props: {
632
573
  metadata,
633
574
  pageProps: normalizedProps,
634
- ...!this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(normalizedProps) } : {}
575
+ ...this.buildNonReactDocumentProps(HtmlTemplate, normalizedProps) ?? {}
635
576
  },
636
577
  children: layoutRender?.html ?? viewRender.html
637
578
  });