@ecopages/react 0.2.0-alpha.4 → 0.2.0-alpha.7

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 (44) hide show
  1. package/CHANGELOG.md +23 -37
  2. package/README.md +143 -17
  3. package/package.json +3 -3
  4. package/src/react-hmr-strategy.d.ts +22 -19
  5. package/src/react-hmr-strategy.js +57 -109
  6. package/src/react-hmr-strategy.ts +76 -134
  7. package/src/react-renderer.d.ts +130 -11
  8. package/src/react-renderer.js +368 -64
  9. package/src/react-renderer.ts +490 -90
  10. package/src/react.plugin.d.ts +17 -5
  11. package/src/react.plugin.js +44 -13
  12. package/src/react.plugin.ts +49 -14
  13. package/src/router-adapter.d.ts +2 -2
  14. package/src/router-adapter.ts +2 -2
  15. package/src/services/react-bundle.service.d.ts +2 -25
  16. package/src/services/react-bundle.service.js +21 -91
  17. package/src/services/react-bundle.service.ts +22 -126
  18. package/src/services/react-hydration-asset.service.js +3 -3
  19. package/src/services/react-hydration-asset.service.ts +7 -4
  20. package/src/services/react-page-module.service.d.ts +3 -0
  21. package/src/services/react-page-module.service.js +20 -16
  22. package/src/services/react-page-module.service.ts +27 -17
  23. package/src/services/react-runtime-bundle.service.d.ts +12 -12
  24. package/src/services/react-runtime-bundle.service.js +98 -180
  25. package/src/services/react-runtime-bundle.service.ts +112 -211
  26. package/src/utils/client-graph-boundary-plugin.js +147 -9
  27. package/src/utils/client-graph-boundary-plugin.ts +252 -11
  28. package/src/utils/hydration-scripts.d.ts +18 -1
  29. package/src/utils/hydration-scripts.js +83 -32
  30. package/src/utils/hydration-scripts.ts +159 -38
  31. package/src/utils/reachability-analyzer.d.ts +12 -1
  32. package/src/utils/reachability-analyzer.js +101 -5
  33. package/src/utils/reachability-analyzer.ts +161 -8
  34. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  35. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  36. package/src/utils/react-dom-runtime-interop-plugin.ts +33 -0
  37. package/src/utils/react-mdx-loader-plugin.js +13 -5
  38. package/src/utils/react-mdx-loader-plugin.ts +28 -5
  39. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  40. package/src/utils/react-runtime-specifier-map.js +37 -0
  41. package/src/utils/react-runtime-specifier-map.ts +45 -0
  42. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  43. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  44. package/src/utils/use-sync-external-store-shim-plugin.ts +45 -0
@@ -8,18 +8,19 @@
8
8
  */
9
9
 
10
10
  import path from 'node:path';
11
- import { pathToFileURL } from 'node:url';
12
11
 
13
12
  import { HmrStrategy, HmrStrategyType, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
14
- import { defaultBuildAdapter } from '@ecopages/core/build/build-adapter';
15
13
  import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
14
+ import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
16
15
  import { FileNotFoundError, fileSystem } from '@ecopages/file-system';
17
16
  import { Logger } from '@ecopages/logger';
18
- import type { DefaultHmrContext, EcoComponentConfig } from '@ecopages/core';
17
+ import type { DefaultHmrContext } from '@ecopages/core';
19
18
  import type { CompileOptions } from '@mdx-js/mdx';
20
19
  import { injectHmrHandler } from './utils/hmr-scripts.ts';
21
20
  import { createClientGraphBoundaryPlugin } from './utils/client-graph-boundary-plugin.ts';
22
21
  import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from './utils/declared-modules.ts';
22
+ import { getReactClientGraphAllowSpecifiers } from './utils/react-runtime-specifier-map.ts';
23
+ import { createUseSyncExternalStoreShimPlugin } from './utils/use-sync-external-store-shim-plugin.ts';
23
24
  import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
24
25
 
25
26
  const appLogger = new Logger('[ReactHmrStrategy]');
@@ -33,9 +34,11 @@ const appLogger = new Logger('[ReactHmrStrategy]');
33
34
  * The processing steps are:
34
35
  * 1. Check if any React entrypoints are registered
35
36
  * 2. Rebuild all React entrypoints (the changed file could be a dependency)
36
- * 3. Replace bare specifiers with runtime URLs
37
- * 4. Inject HMR acceptance handler
38
- * 5. Broadcast update events for each rebuilt entrypoint
37
+ * 3. Rebuild browser output through the shared browser bundle service while
38
+ * preserving React-specific runtime aliases and graph policy
39
+ * 4. Read page config metadata through the shared server-module loading path
40
+ * 5. Inject HMR acceptance handler
41
+ * 6. Broadcast update events for each rebuilt entrypoint
39
42
  *
40
43
  * @remarks
41
44
  * This strategy has higher priority than generic JsHmrStrategy, allowing it
@@ -63,84 +66,13 @@ const appLogger = new Logger('[ReactHmrStrategy]');
63
66
  export class ReactHmrStrategy extends HmrStrategy {
64
67
  readonly type = HmrStrategyType.INTEGRATION;
65
68
  private mdxCompilerOptions?: CompileOptions;
66
- private readonly knownEntrypoints = new Set<string>();
67
-
69
+ private readonly ownedTemplateExtensions: Set<string>;
70
+ private readonly allTemplateExtensions: string[];
68
71
  private async importNodePageModule(entrypointPath: string): Promise<{
69
- default?: { config?: EcoComponentConfig };
70
- config?: EcoComponentConfig;
72
+ default?: { config?: Record<string, unknown> };
73
+ config?: Record<string, unknown>;
71
74
  }> {
72
- const srcDir = this.context.getSrcDir();
73
- const rootDir = path.dirname(srcDir);
74
- const outdir = path.join(path.resolve(this.context.getDistDir(), '..', '..'), '.server-modules');
75
- const fileBaseName = path.basename(entrypointPath, path.extname(entrypointPath));
76
- const fileHash = fileSystem.hash(entrypointPath);
77
- const outputFileName = `${fileBaseName}-${fileHash}.js`;
78
-
79
- const buildResult = await defaultBuildAdapter.build({
80
- entrypoints: [entrypointPath],
81
- root: rootDir,
82
- outdir,
83
- target: 'node',
84
- format: 'esm',
85
- sourcemap: 'none',
86
- splitting: false,
87
- minify: false,
88
- naming: outputFileName,
89
- });
90
-
91
- if (!buildResult.success) {
92
- const details = buildResult.logs.map((log) => log.message).join(' | ');
93
- throw new Error(`Error transpiling React HMR page module: ${details}`);
94
- }
95
-
96
- const preferredOutputPath = path.join(outdir, outputFileName);
97
- const compiledOutput =
98
- buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ??
99
- buildResult.outputs.find((output) => output.path.endsWith('.js'))?.path;
100
-
101
- if (!compiledOutput) {
102
- throw new Error(`No transpiled output generated for React HMR page module: ${entrypointPath}`);
103
- }
104
-
105
- return (await import(pathToFileURL(compiledOutput).href)) as {
106
- default?: { config?: EcoComponentConfig };
107
- config?: EcoComponentConfig;
108
- };
109
- }
110
-
111
- private createUseSyncExternalStoreShimPlugin(): EcoBuildPlugin {
112
- return {
113
- name: 'react-hmr-use-sync-external-store-shim',
114
- setup(build) {
115
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
116
- path: 'use-sync-external-store/shim',
117
- namespace: 'ecopages-react-hmr-shim',
118
- }));
119
-
120
- build.onLoad(
121
- { filter: /^use-sync-external-store\/shim$/, namespace: 'ecopages-react-hmr-shim' },
122
- () => ({
123
- contents: "export { useSyncExternalStore } from 'react';",
124
- loader: 'js',
125
- }),
126
- );
127
-
128
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
129
- contents: "export { useSyncExternalStore } from 'react';",
130
- loader: 'js',
131
- }));
132
-
133
- build.onLoad(
134
- {
135
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/,
136
- },
137
- () => ({
138
- contents: "export { useSyncExternalStore } from 'react';",
139
- loader: 'js',
140
- }),
141
- );
142
- },
143
- };
75
+ return await this.context.importServerModule(entrypointPath);
144
76
  }
145
77
 
146
78
  /**
@@ -156,14 +88,25 @@ export class ReactHmrStrategy extends HmrStrategy {
156
88
  * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
157
89
  * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
158
90
  */
91
+ private context: DefaultHmrContext;
92
+ private pageMetadataCache: ReactHmrPageMetadataCache;
93
+ private explicitGraphEnabled: boolean;
94
+
159
95
  constructor(
160
- private context: DefaultHmrContext,
161
- private pageMetadataCache: ReactHmrPageMetadataCache,
96
+ context: DefaultHmrContext,
97
+ pageMetadataCache: ReactHmrPageMetadataCache,
162
98
  mdxCompilerOptions?: CompileOptions,
163
- private explicitGraphEnabled = false,
99
+ ownedTemplateExtensions: string[] = ['.tsx'],
100
+ allTemplateExtensions: string[] = ['.tsx'],
101
+ explicitGraphEnabled = false,
164
102
  ) {
165
103
  super();
104
+ this.context = context;
105
+ this.pageMetadataCache = pageMetadataCache;
106
+ this.explicitGraphEnabled = explicitGraphEnabled;
166
107
  this.mdxCompilerOptions = mdxCompilerOptions;
108
+ this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
109
+ this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
167
110
  }
168
111
 
169
112
  /**
@@ -173,15 +116,11 @@ export class ReactHmrStrategy extends HmrStrategy {
173
116
  * (including `node:*`) from breaking the browser bundle.
174
117
  */
175
118
  private getBuildPlugins(declaredModules?: string[]): EcoBuildPlugin[] {
176
- const allowSpecifiers = [
177
- '@ecopages/core',
178
- 'react',
179
- 'react-dom',
180
- 'react/jsx-runtime',
181
- 'react/jsx-dev-runtime',
182
- 'react-dom/client',
183
- ...Array.from(this.context.getSpecifierMap().keys()),
184
- ];
119
+ const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.context.getSpecifierMap().keys());
120
+
121
+ const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.context.getSpecifierMap(), {
122
+ name: 'react-hmr-runtime-specifier-alias',
123
+ });
185
124
 
186
125
  return [
187
126
  createClientGraphBoundaryPlugin({
@@ -189,17 +128,50 @@ export class ReactHmrStrategy extends HmrStrategy {
189
128
  alwaysAllowSpecifiers: allowSpecifiers,
190
129
  declaredModules,
191
130
  }),
131
+ ...(runtimeAliasPlugin ? [runtimeAliasPlugin] : []),
192
132
  ...this.context.getPlugins(),
193
- this.createUseSyncExternalStoreShimPlugin(),
133
+ createUseSyncExternalStoreShimPlugin({
134
+ name: 'react-hmr-use-sync-external-store-shim',
135
+ namespace: 'ecopages-react-hmr-shim',
136
+ }),
194
137
  ];
195
138
  }
196
139
 
197
140
  private isReactEntrypoint(filePath: string): boolean {
198
- if (filePath.endsWith('.tsx')) {
141
+ if (filePath.endsWith('.mdx')) {
142
+ return this.mdxCompilerOptions !== undefined;
143
+ }
144
+
145
+ if (!filePath.endsWith('.tsx')) {
146
+ return false;
147
+ }
148
+
149
+ if (!this.isRouteTemplate(filePath)) {
199
150
  return true;
200
151
  }
201
152
 
202
- return filePath.endsWith('.mdx') && this.mdxCompilerOptions !== undefined;
153
+ const templateExtension = this.resolveTemplateExtension(filePath);
154
+ if (!templateExtension) {
155
+ return false;
156
+ }
157
+
158
+ return this.ownedTemplateExtensions.has(templateExtension);
159
+ }
160
+
161
+ /**
162
+ * Returns true when a route file uses a compound extension like `page.foo.tsx`.
163
+ *
164
+ * @remarks
165
+ * React integration owns plain `.tsx` route templates. Compound extensions in
166
+ * pages/layouts are integration-specific route templates and should not be
167
+ * claimed by React HMR strategy.
168
+ */
169
+ private isRouteTemplate(filePath: string): boolean {
170
+ return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
171
+ }
172
+
173
+ private resolveTemplateExtension(filePath: string): string | undefined {
174
+ return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
203
175
  }
204
176
 
205
177
  /**
@@ -258,11 +230,10 @@ export class ReactHmrStrategy extends HmrStrategy {
258
230
  appLogger.debug(`Detected layout file change: ${_filePath}`);
259
231
  }
260
232
 
261
- const entrypointsToBuild =
262
- !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath)
263
- ? [[_filePath, watchedFiles.get(_filePath)!]]
264
- : watchedFiles.entries();
265
- this.knownEntrypoints.add(_filePath);
233
+ const changedEntrypointOutput = watchedFiles.get(_filePath);
234
+ const entrypointsToBuild = changedEntrypointOutput
235
+ ? [[_filePath, changedEntrypointOutput]]
236
+ : watchedFiles.entries();
266
237
 
267
238
  const updates: string[] = [];
268
239
  for (const [entrypoint, outputUrl] of entrypointsToBuild) {
@@ -335,16 +306,13 @@ export class ReactHmrStrategy extends HmrStrategy {
335
306
  plugins.unshift(mdxPlugin);
336
307
  }
337
308
 
338
- const result = await defaultBuildAdapter.build({
309
+ const result = await this.context.getBrowserBundleService().bundle({
310
+ profile: 'hmr-entrypoint',
339
311
  entrypoints: [entrypointPath],
340
312
  outdir: tempDir,
341
313
  naming: `[name].[hash].tmp`,
342
- target: 'browser',
343
- format: 'esm',
344
- sourcemap: 'none',
345
314
  plugins,
346
315
  minify: false,
347
- external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom/client'],
348
316
  });
349
317
 
350
318
  if (!result.success) {
@@ -375,7 +343,7 @@ export class ReactHmrStrategy extends HmrStrategy {
375
343
  }
376
344
 
377
345
  /**
378
- * Processes bundled output by replacing specifiers and injecting HMR handler.
346
+ * Processes bundled output and injects the React HMR handler.
379
347
  * Writes to temp file first, then renames atomically to avoid conflicts.
380
348
  *
381
349
  * @param tempPath - Path to the temporary bundled file
@@ -392,7 +360,6 @@ export class ReactHmrStrategy extends HmrStrategy {
392
360
  try {
393
361
  let code = await fileSystem.readFile(tempPath);
394
362
 
395
- code = this.replaceBareSpecifiers(code);
396
363
  code = injectHmrHandler(code);
397
364
 
398
365
  await fileSystem.writeAsync(finalPath, code);
@@ -416,29 +383,4 @@ export class ReactHmrStrategy extends HmrStrategy {
416
383
  return false;
417
384
  }
418
385
  }
419
-
420
- /**
421
- * Replaces bare specifiers with runtime URLs.
422
- *
423
- * Handles both static imports and dynamic imports.
424
- *
425
- * @param code - The bundled code to transform
426
- * @returns The transformed code with runtime URLs
427
- */
428
- private replaceBareSpecifiers(code: string): string {
429
- const specifierMap = this.context.getSpecifierMap();
430
-
431
- if (specifierMap.size === 0) {
432
- return code;
433
- }
434
-
435
- let result = code;
436
- for (const [bareSpec, runtimeUrl] of specifierMap.entries()) {
437
- const escaped = bareSpec.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
438
- result = result.replace(new RegExp(`from\\s*["']${escaped}["']`, 'g'), `from "${runtimeUrl}"`);
439
- result = result.replace(new RegExp(`import\\(["']${escaped}["']\\)`, 'g'), `import("${runtimeUrl}")`);
440
- }
441
-
442
- return result;
443
- }
444
386
  }
@@ -12,6 +12,11 @@ import { ReactBundleService } from './services/react-bundle.service.js';
12
12
  import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
13
13
  import { ReactPageModuleService } from './services/react-page-module.service.js';
14
14
  import { ReactHydrationAssetService } from './services/react-hydration-asset.service.js';
15
+ type ReactPageModule = EcoPageFile<{
16
+ config?: EcoComponentConfig;
17
+ }> & {
18
+ config?: EcoComponentConfig;
19
+ };
15
20
  /**
16
21
  * Error thrown when an error occurs while rendering a React component.
17
22
  */
@@ -32,7 +37,6 @@ export declare class BundleError extends Error {
32
37
  export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
33
38
  name: string;
34
39
  componentDirectory: string;
35
- private componentRenderSequence;
36
40
  static routerAdapter: ReactRouterAdapter | undefined;
37
41
  static mdxCompilerOptions: CompileOptions | undefined;
38
42
  static mdxExtensions: string[];
@@ -57,14 +61,101 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
57
61
  runtimeOrigin: string;
58
62
  });
59
63
  protected shouldRenderPageComponent(): boolean;
64
+ /**
65
+ * Reads the declared integration name for a component or layout.
66
+ *
67
+ * We honor both the explicit `config.integration` override and injected
68
+ * `config.__eco.integration` metadata because pages can arrive here through
69
+ * authored config as well as build-time component metadata.
70
+ */
71
+ private getComponentIntegration;
72
+ /**
73
+ * Returns whether a component should stay inside the React render lane.
74
+ *
75
+ * Components without explicit integration metadata are treated as React-owned
76
+ * here because this renderer only receives them after the route pipeline has
77
+ * already selected the React integration.
78
+ */
79
+ private isReactManagedComponent;
80
+ /**
81
+ * Creates the canonical page-props payload used by router hydration.
82
+ *
83
+ * React pages embedded in a non-React HTML shell still need to expose the same
84
+ * page-data contract as fully React-owned documents so navigation and hydration
85
+ * can read one marker consistently.
86
+ */
87
+ private buildRouterPageDataScript;
88
+ private getRouterDocumentAttributes;
89
+ /**
90
+ * Commits a framework-agnostic component to React semantics.
91
+ *
92
+ * This is one of the two real cast boundaries in this file. Core keeps
93
+ * `EcoComponent` broad so integrations can share the same public surface; once
94
+ * the React renderer is executing, `createElement()` needs a concrete React
95
+ * component signature.
96
+ */
97
+ private asReactComponent;
98
+ /**
99
+ * Commits a mixed-shell component to the string-returning contract required by
100
+ * non-React layouts and HTML templates.
101
+ *
102
+ * This is the second real cast boundary: once we decide a shell is not managed
103
+ * by React, we call it directly and require serialized HTML back.
104
+ */
105
+ private asNonReactShellComponent;
106
+ /**
107
+ * Builds the serialized page-props payload embedded into the final HTML.
108
+ *
109
+ * The document payload is intentionally narrower than the full server render
110
+ * input: only routing data, public page props, and explicitly allowed locals are
111
+ * exposed to the browser.
112
+ */
113
+ private buildSerializedPageProps;
114
+ /**
115
+ * Appends route hydration assets for a concrete page/view file to the current
116
+ * HTML transformer state.
117
+ */
118
+ private appendHydrationAssetsForFile;
119
+ /**
120
+ * Resolves metadata for direct `renderToResponse()` calls.
121
+ *
122
+ * View rendering bypasses the normal route-file pipeline, so metadata has to be
123
+ * evaluated here from either the component-level generator or the application
124
+ * default.
125
+ */
126
+ private resolveViewMetadata;
127
+ /**
128
+ * Renders a non-React layout or HTML template and enforces that mixed shells
129
+ * return serialized HTML.
130
+ *
131
+ * The React renderer can compose through another integration's shell, but only
132
+ * if that shell yields a string that can be inserted into the final document.
133
+ */
134
+ private renderNonReactShellComponent;
135
+ /**
136
+ * Produces the page body before the final HTML template is applied.
137
+ *
138
+ * This method owns the React/non-React layout split. React-managed layouts stay
139
+ * as React elements so they can stream normally; non-React layouts are rendered
140
+ * to HTML first and then passed through as serialized content.
141
+ */
142
+ private composePageContent;
143
+ /**
144
+ * Wraps composed page content in the final document template.
145
+ *
146
+ * React-owned HTML templates stream directly. Non-React templates receive
147
+ * pre-rendered page HTML plus the canonical React page-data payload so the
148
+ * client runtime can recover page data after cross-integration handoff.
149
+ */
150
+ private renderDocument;
60
151
  /**
61
152
  * Renders a React component for component-level orchestration.
62
153
  *
63
154
  * Behavior:
64
155
  * - 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.
156
+ * - When an explicit component instance id is provided, a stable
157
+ * `data-eco-component-id` attribute is attached so island hydration can target it.
158
+ * - Without an explicit instance id, component renders remain plain SSR output.
68
159
  *
69
160
  * This preserves DOM shape for global CSS/layout selectors while keeping a
70
161
  * deterministic mount target per component instance.
@@ -82,25 +173,53 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
82
173
  * @returns Processed assets for MDX configuration dependencies
83
174
  */
84
175
  private processMdxConfigDependencies;
176
+ private processDeclaredMdxSsrLazyDependencies;
177
+ private collectDeclaredMdxSsrLazyDependencies;
85
178
  buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
86
- protected importPageFile(file: string): Promise<EcoPageFile<{
87
- config?: EcoComponentConfig;
88
- }>>;
179
+ /**
180
+ * Imports a page module while normalizing React MDX modules to the same shape
181
+ * as ordinary React page files.
182
+ *
183
+ * MDX page imports can expose `config` separately from the default export. The
184
+ * React renderer reattaches that config to the page component so downstream
185
+ * layout, dependency, and hydration logic can treat MDX and TSX pages the same.
186
+ */
187
+ protected importPageFile(file: string): Promise<ReactPageModule>;
188
+ /**
189
+ * Renders a full route response for the filesystem page pipeline.
190
+ *
191
+ * This path receives already-resolved route metadata, layout, locals, and HTML
192
+ * template instances from the shared renderer orchestration. Its main job is to
193
+ * serialize only the browser-safe page payload, compose the mixed React/non-
194
+ * React shell tree, and hand the result back as a document body.
195
+ */
89
196
  render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody>;
197
+ protected getDocumentAttributes(): Record<string, string> | undefined;
90
198
  /**
91
- * Safely extracts locals for client-side hydration.
199
+ * Safely extracts the declared subset of locals for client-side hydration.
92
200
  *
93
201
  * 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.
202
+ * request-scoped data (e.g., session). Only keys explicitly declared via
203
+ * `Page.requires` are serialized to the client so sensitive request-only data
204
+ * is not leaked into hydration payloads by default.
96
205
  *
97
206
  * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
98
207
  * to prevent accidental use. This method safely detects that case and returns
99
208
  * `undefined` instead of throwing.
100
209
  *
101
210
  * @param locals - The locals object from the render context
102
- * @returns The locals object if serializable, undefined otherwise
211
+ * @param requiredLocals - Keys explicitly requested for client hydration
212
+ * @returns The filtered locals object if serializable, undefined otherwise
103
213
  */
104
214
  private getSerializableLocals;
215
+ /**
216
+ * Renders an arbitrary React view through the application's HTML shell.
217
+ *
218
+ * Unlike route rendering, this path starts from a single component rather than a
219
+ * page module discovered by the router. It still needs to resolve metadata,
220
+ * layout dependencies, and hydration assets so direct `ctx.render()` calls match
221
+ * normal page responses.
222
+ */
105
223
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
106
224
  }
225
+ export {};