@ecopages/react 0.2.0-alpha.1 → 0.2.0-alpha.11

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 (50) hide show
  1. package/CHANGELOG.md +9 -43
  2. package/README.md +143 -17
  3. package/package.json +3 -3
  4. package/src/react-hmr-strategy.d.ts +25 -21
  5. package/src/react-hmr-strategy.js +78 -110
  6. package/src/react-renderer.d.ts +135 -12
  7. package/src/react-renderer.js +439 -82
  8. package/src/react.plugin.d.ts +17 -5
  9. package/src/react.plugin.js +45 -13
  10. package/src/router-adapter.d.ts +2 -2
  11. package/src/services/react-bundle.service.d.ts +4 -25
  12. package/src/services/react-bundle.service.js +37 -91
  13. package/src/services/react-hydration-asset.service.js +3 -3
  14. package/src/services/react-page-module.service.d.ts +3 -0
  15. package/src/services/react-page-module.service.js +24 -17
  16. package/src/services/react-runtime-bundle.service.d.ts +12 -12
  17. package/src/services/react-runtime-bundle.service.js +98 -180
  18. package/src/utils/client-graph-boundary-plugin.js +149 -11
  19. package/src/utils/declared-modules.js +4 -1
  20. package/src/utils/foreign-jsx-override-plugin.d.ts +19 -0
  21. package/src/utils/foreign-jsx-override-plugin.js +43 -0
  22. package/src/utils/hydration-scripts.d.ts +18 -1
  23. package/src/utils/hydration-scripts.js +95 -37
  24. package/src/utils/reachability-analyzer.d.ts +12 -1
  25. package/src/utils/reachability-analyzer.js +101 -5
  26. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  27. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  28. package/src/utils/react-mdx-loader-plugin.js +13 -5
  29. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  30. package/src/utils/react-runtime-specifier-map.js +37 -0
  31. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  32. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  33. package/src/react-hmr-strategy.ts +0 -444
  34. package/src/react-renderer.ts +0 -403
  35. package/src/react.plugin.ts +0 -241
  36. package/src/router-adapter.ts +0 -95
  37. package/src/services/react-bundle.service.ts +0 -212
  38. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  39. package/src/services/react-hydration-asset.service.ts +0 -260
  40. package/src/services/react-page-module.service.ts +0 -214
  41. package/src/services/react-runtime-bundle.service.ts +0 -271
  42. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  43. package/src/utils/client-only.ts +0 -27
  44. package/src/utils/declared-modules.ts +0 -99
  45. package/src/utils/dynamic.ts +0 -27
  46. package/src/utils/hmr-scripts.ts +0 -47
  47. package/src/utils/html-boundary.ts +0 -66
  48. package/src/utils/hydration-scripts.ts +0 -338
  49. package/src/utils/reachability-analyzer.ts +0 -440
  50. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,14 +1,22 @@
1
1
  import path from "node:path";
2
- import { pathToFileURL } from "node:url";
3
2
  import { HmrStrategy, HmrStrategyType } from "@ecopages/core/hmr/hmr-strategy";
4
- import { defaultBuildAdapter } from "@ecopages/core/build/build-adapter";
3
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
5
4
  import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
6
5
  import { Logger } from "@ecopages/logger";
7
6
  import { injectHmrHandler } from "./utils/hmr-scripts.js";
8
7
  import { createClientGraphBoundaryPlugin } from "./utils/client-graph-boundary-plugin.js";
9
8
  import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from "./utils/declared-modules.js";
9
+ import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-specifier-map.js";
10
+ import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
10
11
  const appLogger = new Logger("[ReactHmrStrategy]");
11
12
  class ReactHmrStrategy extends HmrStrategy {
13
+ type = HmrStrategyType.INTEGRATION;
14
+ mdxCompilerOptions;
15
+ ownedTemplateExtensions;
16
+ allTemplateExtensions;
17
+ async importNodePageModule(entrypointPath) {
18
+ return await this.context.importServerModule(entrypointPath);
19
+ }
12
20
  /**
13
21
  * Creates a new React HMR strategy instance.
14
22
  *
@@ -22,75 +30,17 @@ class ReactHmrStrategy extends HmrStrategy {
22
30
  * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
23
31
  * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
24
32
  */
25
- constructor(context, pageMetadataCache, mdxCompilerOptions, explicitGraphEnabled = false) {
33
+ context;
34
+ pageMetadataCache;
35
+ explicitGraphEnabled;
36
+ constructor(context, pageMetadataCache, mdxCompilerOptions, ownedTemplateExtensions = [".tsx"], allTemplateExtensions = [".tsx"], explicitGraphEnabled = false) {
26
37
  super();
27
38
  this.context = context;
28
39
  this.pageMetadataCache = pageMetadataCache;
29
40
  this.explicitGraphEnabled = explicitGraphEnabled;
30
41
  this.mdxCompilerOptions = mdxCompilerOptions;
31
- }
32
- type = HmrStrategyType.INTEGRATION;
33
- mdxCompilerOptions;
34
- knownEntrypoints = /* @__PURE__ */ new Set();
35
- async importNodePageModule(entrypointPath) {
36
- const srcDir = this.context.getSrcDir();
37
- const rootDir = path.dirname(srcDir);
38
- const outdir = path.join(path.resolve(this.context.getDistDir(), "..", ".."), ".server-modules");
39
- const fileBaseName = path.basename(entrypointPath, path.extname(entrypointPath));
40
- const fileHash = fileSystem.hash(entrypointPath);
41
- const outputFileName = `${fileBaseName}-${fileHash}.js`;
42
- const buildResult = await defaultBuildAdapter.build({
43
- entrypoints: [entrypointPath],
44
- root: rootDir,
45
- outdir,
46
- target: "node",
47
- format: "esm",
48
- sourcemap: "none",
49
- splitting: false,
50
- minify: false,
51
- naming: outputFileName
52
- });
53
- if (!buildResult.success) {
54
- const details = buildResult.logs.map((log) => log.message).join(" | ");
55
- throw new Error(`Error transpiling React HMR page module: ${details}`);
56
- }
57
- const preferredOutputPath = path.join(outdir, outputFileName);
58
- const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => output.path.endsWith(".js"))?.path;
59
- if (!compiledOutput) {
60
- throw new Error(`No transpiled output generated for React HMR page module: ${entrypointPath}`);
61
- }
62
- return await import(pathToFileURL(compiledOutput).href);
63
- }
64
- createUseSyncExternalStoreShimPlugin() {
65
- return {
66
- name: "react-hmr-use-sync-external-store-shim",
67
- setup(build) {
68
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
69
- path: "use-sync-external-store/shim",
70
- namespace: "ecopages-react-hmr-shim"
71
- }));
72
- build.onLoad(
73
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-hmr-shim" },
74
- () => ({
75
- contents: "export { useSyncExternalStore } from 'react';",
76
- loader: "js"
77
- })
78
- );
79
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
80
- contents: "export { useSyncExternalStore } from 'react';",
81
- loader: "js"
82
- }));
83
- build.onLoad(
84
- {
85
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
86
- },
87
- () => ({
88
- contents: "export { useSyncExternalStore } from 'react';",
89
- loader: "js"
90
- })
91
- );
92
- }
93
- };
42
+ this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
43
+ this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
94
44
  }
95
45
  /**
96
46
  * Returns build plugins for React HMR bundling.
@@ -99,30 +49,53 @@ class ReactHmrStrategy extends HmrStrategy {
99
49
  * (including `node:*`) from breaking the browser bundle.
100
50
  */
101
51
  getBuildPlugins(declaredModules) {
102
- const allowSpecifiers = [
103
- "@ecopages/core",
104
- "react",
105
- "react-dom",
106
- "react/jsx-runtime",
107
- "react/jsx-dev-runtime",
108
- "react-dom/client",
109
- ...Array.from(this.context.getSpecifierMap().keys())
110
- ];
52
+ const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
53
+ const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
54
+ name: "react-hmr-runtime-specifier-alias"
55
+ });
111
56
  return [
112
57
  createClientGraphBoundaryPlugin({
113
58
  absWorkingDir: path.dirname(this.context.getSrcDir()),
114
59
  alwaysAllowSpecifiers: allowSpecifiers,
115
60
  declaredModules
116
61
  }),
62
+ ...runtimeAliasPlugin ? [runtimeAliasPlugin] : [],
117
63
  ...this.context.getPlugins(),
118
- this.createUseSyncExternalStoreShimPlugin()
64
+ createUseSyncExternalStoreShimPlugin({
65
+ name: "react-hmr-use-sync-external-store-shim",
66
+ namespace: "ecopages-react-hmr-shim"
67
+ })
119
68
  ];
120
69
  }
121
70
  isReactEntrypoint(filePath) {
122
- if (filePath.endsWith(".tsx")) {
71
+ if (filePath.endsWith(".mdx")) {
72
+ return this.mdxCompilerOptions !== void 0;
73
+ }
74
+ if (!filePath.endsWith(".tsx")) {
75
+ return false;
76
+ }
77
+ if (!this.isRouteTemplate(filePath)) {
123
78
  return true;
124
79
  }
125
- return filePath.endsWith(".mdx") && this.mdxCompilerOptions !== void 0;
80
+ const templateExtension = this.resolveTemplateExtension(filePath);
81
+ if (!templateExtension) {
82
+ return false;
83
+ }
84
+ return this.ownedTemplateExtensions.has(templateExtension);
85
+ }
86
+ /**
87
+ * Returns true when a route file uses a compound extension like `page.foo.tsx`.
88
+ *
89
+ * @remarks
90
+ * React integration owns plain `.tsx` route templates. Compound extensions in
91
+ * pages/layouts are integration-specific route templates and should not be
92
+ * claimed by React HMR strategy.
93
+ */
94
+ isRouteTemplate(filePath) {
95
+ return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
96
+ }
97
+ resolveTemplateExtension(filePath) {
98
+ return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
126
99
  }
127
100
  /**
128
101
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
@@ -174,8 +147,8 @@ class ReactHmrStrategy extends HmrStrategy {
174
147
  if (isLayout) {
175
148
  appLogger.debug(`Detected layout file change: ${_filePath}`);
176
149
  }
177
- const entrypointsToBuild = !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath) ? [[_filePath, watchedFiles.get(_filePath)]] : watchedFiles.entries();
178
- this.knownEntrypoints.add(_filePath);
150
+ const changedEntrypointOutput = watchedFiles.get(_filePath);
151
+ const entrypointsToBuild = changedEntrypointOutput ? [[_filePath, changedEntrypointOutput]] : watchedFiles.entries();
179
152
  const updates = [];
180
153
  for (const [entrypoint, outputUrl] of entrypointsToBuild) {
181
154
  if (!this.isReactEntrypoint(entrypoint)) {
@@ -235,16 +208,13 @@ class ReactHmrStrategy extends HmrStrategy {
235
208
  const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
236
209
  plugins.unshift(mdxPlugin);
237
210
  }
238
- const result = await defaultBuildAdapter.build({
211
+ const result = await this.context.getBrowserBundleService().bundle({
212
+ profile: "hmr-entrypoint",
239
213
  entrypoints: [entrypointPath],
240
214
  outdir: tempDir,
241
215
  naming: `[name].[hash].tmp`,
242
- target: "browser",
243
- format: "esm",
244
- sourcemap: "none",
245
216
  plugins,
246
- minify: false,
247
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"]
217
+ minify: false
248
218
  });
249
219
  if (!result.success) {
250
220
  appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
@@ -255,13 +225,33 @@ class ReactHmrStrategy extends HmrStrategy {
255
225
  appLogger.error(`No output file generated for ${entrypointPath}`);
256
226
  return false;
257
227
  }
258
- const processed = await this.processOutput(tempFile, outputPath, outputUrl);
228
+ const resolvedTempFile = await this.resolveTempOutputPath(tempFile);
229
+ if (!resolvedTempFile) {
230
+ appLogger.debug(`Skipping stale temp output for ${outputUrl}: ${tempFile}`);
231
+ return false;
232
+ }
233
+ const processed = await this.processOutput(resolvedTempFile, outputPath, outputUrl);
259
234
  return processed;
260
235
  } catch (error) {
261
236
  appLogger.error(`Error bundling ${entrypointPath}:`, error);
262
237
  return false;
263
238
  }
264
239
  }
240
+ async resolveTempOutputPath(tempPath) {
241
+ if (fileSystem.exists(tempPath)) {
242
+ return tempPath;
243
+ }
244
+ if (!tempPath.includes("[hash]")) {
245
+ return tempPath;
246
+ }
247
+ const directory = path.dirname(tempPath);
248
+ const pattern = path.basename(tempPath).replaceAll("[hash]", "*");
249
+ const matches = await fileSystem.glob([pattern], { cwd: directory });
250
+ if (matches.length === 0) {
251
+ return null;
252
+ }
253
+ return path.isAbsolute(matches[0]) ? matches[0] : path.join(directory, matches[0]);
254
+ }
265
255
  /**
266
256
  * Encodes dynamic route segments (brackets) in file paths.
267
257
  * Converts `[slug]` to `_slug_` to avoid filesystem issues.
@@ -270,7 +260,7 @@ class ReactHmrStrategy extends HmrStrategy {
270
260
  return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
271
261
  }
272
262
  /**
273
- * Processes bundled output by replacing specifiers and injecting HMR handler.
263
+ * Processes bundled output and injects the React HMR handler.
274
264
  * Writes to temp file first, then renames atomically to avoid conflicts.
275
265
  *
276
266
  * @param tempPath - Path to the temporary bundled file
@@ -285,7 +275,6 @@ class ReactHmrStrategy extends HmrStrategy {
285
275
  }
286
276
  try {
287
277
  let code = await fileSystem.readFile(tempPath);
288
- code = this.replaceBareSpecifiers(code);
289
278
  code = injectHmrHandler(code);
290
279
  await fileSystem.writeAsync(finalPath, code);
291
280
  await fileSystem.removeAsync(tempPath).catch(() => {
@@ -305,27 +294,6 @@ class ReactHmrStrategy extends HmrStrategy {
305
294
  return false;
306
295
  }
307
296
  }
308
- /**
309
- * Replaces bare specifiers with runtime URLs.
310
- *
311
- * Handles both static imports and dynamic imports.
312
- *
313
- * @param code - The bundled code to transform
314
- * @returns The transformed code with runtime URLs
315
- */
316
- replaceBareSpecifiers(code) {
317
- const specifierMap = this.context.getSpecifierMap();
318
- if (specifierMap.size === 0) {
319
- return code;
320
- }
321
- let result = code;
322
- for (const [bareSpec, runtimeUrl] of specifierMap.entries()) {
323
- const escaped = bareSpec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
324
- result = result.replace(new RegExp(`from\\s*["']${escaped}["']`, "g"), `from "${runtimeUrl}"`);
325
- result = result.replace(new RegExp(`import\\(["']${escaped}["']\\)`, "g"), `import("${runtimeUrl}")`);
326
- }
327
- return result;
328
- }
329
297
  }
330
298
  export {
331
299
  ReactHmrStrategy
@@ -2,7 +2,7 @@
2
2
  * This module contains the React renderer
3
3
  * @module
4
4
  */
5
- import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentConfig, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
6
6
  import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
7
7
  import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
8
8
  import { type ReactNode } from 'react';
@@ -32,7 +32,6 @@ export declare class BundleError extends Error {
32
32
  export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
33
33
  name: string;
34
34
  componentDirectory: string;
35
- private componentRenderSequence;
36
35
  static routerAdapter: ReactRouterAdapter | undefined;
37
36
  static mdxCompilerOptions: CompileOptions | undefined;
38
37
  static mdxExtensions: string[];
@@ -57,14 +56,117 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
57
56
  runtimeOrigin: string;
58
57
  });
59
58
  protected shouldRenderPageComponent(): boolean;
59
+ /**
60
+ * Reads the declared integration name for a component or layout.
61
+ *
62
+ * We honor both the explicit `config.integration` override and injected
63
+ * `config.__eco.integration` metadata because pages can arrive here through
64
+ * authored config as well as build-time component metadata.
65
+ */
66
+ private getComponentIntegration;
67
+ /**
68
+ * Returns whether a component should stay inside the React render lane.
69
+ *
70
+ * Components without explicit integration metadata are treated as React-owned
71
+ * here because this renderer only receives them after the route pipeline has
72
+ * already selected the React integration.
73
+ */
74
+ private isReactManagedComponent;
75
+ /**
76
+ * Creates the canonical page-props payload used by router hydration.
77
+ *
78
+ * React pages embedded in a non-React HTML shell still need to expose the same
79
+ * page-data contract as fully React-owned documents so navigation and hydration
80
+ * can read one marker consistently.
81
+ */
82
+ private buildRouterPageDataScript;
83
+ private getRouterDocumentAttributes;
84
+ /**
85
+ * Commits a framework-agnostic component to React semantics.
86
+ *
87
+ * This is one of the two real cast boundaries in this file. Core keeps
88
+ * `EcoComponent` broad so integrations can share the same public surface; once
89
+ * the React renderer is executing, `createElement()` needs a concrete React
90
+ * component signature.
91
+ */
92
+ private asReactComponent;
93
+ /**
94
+ * Commits a mixed-shell component to the string-returning contract required by
95
+ * non-React layouts and HTML templates.
96
+ *
97
+ * This is the second real cast boundary: once we decide a shell is not managed
98
+ * by React, we call it directly and require serialized HTML back.
99
+ */
100
+ private asNonReactShellComponent;
101
+ /**
102
+ * Builds the serialized page-props payload embedded into the final HTML.
103
+ *
104
+ * The document payload is intentionally narrower than the full server render
105
+ * input: only routing data, public page props, and explicitly allowed locals are
106
+ * exposed to the browser.
107
+ */
108
+ private buildSerializedPageProps;
109
+ /**
110
+ * Appends route hydration assets for a concrete page/view file to the current
111
+ * HTML transformer state.
112
+ */
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
+ /**
123
+ * Renders a non-React layout or HTML template and enforces that mixed shells
124
+ * return serialized HTML.
125
+ *
126
+ * The React renderer can compose through another integration's shell, but only
127
+ * if that shell yields a string that can be inserted into the final document.
128
+ */
129
+ private renderNonReactShellComponent;
130
+ /**
131
+ * Renders one React component boundary for marker-graph orchestration.
132
+ *
133
+ * When the marker resolver has already stitched 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 stitched HTML afterward.
137
+ *
138
+ * @param input Component render input reconstructed from marker metadata.
139
+ * @param context React-specific render context for stable token generation.
140
+ * @returns Serialized component HTML with stitched child markup preserved.
141
+ */
142
+ private renderComponentHtml;
143
+ 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;
60
160
  /**
61
161
  * Renders a React component for component-level orchestration.
62
162
  *
63
163
  * Behavior:
64
164
  * - SSR always returns the component's own root HTML (no synthetic wrapper).
65
- * - For single-root output, a stable `data-eco-component-id` attribute is attached
66
- * to the root element so the client island runtime can target it directly.
67
- * - Island client scripts are emitted through `assets` and mounted independently.
165
+ * - When an explicit component instance id is provided, a stable
166
+ * `data-eco-component-id` attribute is attached so island hydration can target it.
167
+ * - Without an explicit instance id, component renders remain plain SSR output.
168
+ * - When stitched child HTML is provided, that boundary is treated as a pure SSR
169
+ * composition step and does not emit hydration assets for the parent wrapper.
68
170
  *
69
171
  * This preserves DOM shape for global CSS/layout selectors while keeping a
70
172
  * deterministic mount target per component instance.
@@ -76,31 +178,52 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
76
178
  * @returns True if the file is an MDX file
77
179
  */
78
180
  isMdxFile(filePath: string): boolean;
181
+ protected usesIntegrationPageImporter(file: string): boolean;
182
+ protected importIntegrationPageFile(file: string): Promise<EcoPageFile>;
183
+ protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
79
184
  /**
80
185
  * Processes MDX-specific configuration dependencies including layout dependencies.
81
186
  * @param pagePath - Absolute path to the MDX page file
82
187
  * @returns Processed assets for MDX configuration dependencies
83
188
  */
84
189
  private processMdxConfigDependencies;
190
+ private processDeclaredMdxSsrLazyDependencies;
191
+ private collectDeclaredMdxSsrLazyDependencies;
85
192
  buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
86
- protected importPageFile(file: string): Promise<EcoPageFile<{
87
- config?: EcoComponentConfig;
88
- }>>;
193
+ /**
194
+ * Renders a full route response for the filesystem page pipeline.
195
+ *
196
+ * This path receives already-resolved route metadata, layout, locals, and HTML
197
+ * template instances from the shared renderer orchestration. Its main job is to
198
+ * serialize only the browser-safe page payload, compose the mixed React/non-
199
+ * React shell tree, and hand the result back as a document body.
200
+ */
89
201
  render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody>;
202
+ protected getDocumentAttributes(): Record<string, string> | undefined;
90
203
  /**
91
- * Safely extracts locals for client-side hydration.
204
+ * Safely extracts the declared subset of locals for client-side hydration.
92
205
  *
93
206
  * On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
94
- * request-scoped data (e.g., session). This data needs to be serialized to the
95
- * client for hydration to match the server-rendered output.
207
+ * request-scoped data (e.g., session). Only keys explicitly declared via
208
+ * `Page.requires` are serialized to the client so sensitive request-only data
209
+ * is not leaked into hydration payloads by default.
96
210
  *
97
211
  * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
98
212
  * to prevent accidental use. This method safely detects that case and returns
99
213
  * `undefined` instead of throwing.
100
214
  *
101
215
  * @param locals - The locals object from the render context
102
- * @returns The locals object if serializable, undefined otherwise
216
+ * @param requiredLocals - Keys explicitly requested for client hydration
217
+ * @returns The filtered locals object if serializable, undefined otherwise
103
218
  */
104
219
  private getSerializableLocals;
220
+ /**
221
+ * Renders an arbitrary React view through the application's HTML shell.
222
+ *
223
+ * Unlike route rendering, this path starts from a single component rather than a
224
+ * page module discovered by the router. It still needs to resolve metadata,
225
+ * layout dependencies, and hydration assets so direct `ctx.render()` calls match
226
+ * normal page responses.
227
+ */
105
228
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
106
229
  }