@ecopages/react 0.2.0-alpha.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 (59) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +65 -0
  4. package/package.json +76 -0
  5. package/src/declarations.d.ts +6 -0
  6. package/src/react-hmr-strategy.d.ts +143 -0
  7. package/src/react-hmr-strategy.js +332 -0
  8. package/src/react-hmr-strategy.ts +444 -0
  9. package/src/react-renderer.d.ts +106 -0
  10. package/src/react-renderer.js +302 -0
  11. package/src/react-renderer.ts +403 -0
  12. package/src/react.plugin.d.ts +147 -0
  13. package/src/react.plugin.js +126 -0
  14. package/src/react.plugin.ts +241 -0
  15. package/src/router-adapter.d.ts +87 -0
  16. package/src/router-adapter.js +0 -0
  17. package/src/router-adapter.ts +95 -0
  18. package/src/services/react-bundle.service.d.ts +68 -0
  19. package/src/services/react-bundle.service.js +145 -0
  20. package/src/services/react-bundle.service.ts +212 -0
  21. package/src/services/react-hmr-page-metadata-cache.d.ts +17 -0
  22. package/src/services/react-hmr-page-metadata-cache.js +19 -0
  23. package/src/services/react-hmr-page-metadata-cache.ts +24 -0
  24. package/src/services/react-hydration-asset.service.d.ts +75 -0
  25. package/src/services/react-hydration-asset.service.js +198 -0
  26. package/src/services/react-hydration-asset.service.ts +260 -0
  27. package/src/services/react-page-module.service.d.ts +80 -0
  28. package/src/services/react-page-module.service.js +155 -0
  29. package/src/services/react-page-module.service.ts +214 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +38 -0
  31. package/src/services/react-runtime-bundle.service.js +207 -0
  32. package/src/services/react-runtime-bundle.service.ts +271 -0
  33. package/src/utils/client-graph-boundary-plugin.d.ts +43 -0
  34. package/src/utils/client-graph-boundary-plugin.js +356 -0
  35. package/src/utils/client-graph-boundary-plugin.ts +590 -0
  36. package/src/utils/client-only.d.ts +8 -0
  37. package/src/utils/client-only.js +19 -0
  38. package/src/utils/client-only.ts +27 -0
  39. package/src/utils/declared-modules.d.ts +42 -0
  40. package/src/utils/declared-modules.js +56 -0
  41. package/src/utils/declared-modules.ts +99 -0
  42. package/src/utils/dynamic.d.ts +15 -0
  43. package/src/utils/dynamic.js +12 -0
  44. package/src/utils/dynamic.ts +27 -0
  45. package/src/utils/hmr-scripts.d.ts +18 -0
  46. package/src/utils/hmr-scripts.js +31 -0
  47. package/src/utils/hmr-scripts.ts +47 -0
  48. package/src/utils/html-boundary.d.ts +7 -0
  49. package/src/utils/html-boundary.js +55 -0
  50. package/src/utils/html-boundary.ts +66 -0
  51. package/src/utils/hydration-scripts.d.ts +71 -0
  52. package/src/utils/hydration-scripts.js +222 -0
  53. package/src/utils/hydration-scripts.ts +338 -0
  54. package/src/utils/reachability-analyzer.d.ts +55 -0
  55. package/src/utils/reachability-analyzer.js +243 -0
  56. package/src/utils/reachability-analyzer.ts +440 -0
  57. package/src/utils/react-mdx-loader-plugin.d.ts +3 -0
  58. package/src/utils/react-mdx-loader-plugin.js +37 -0
  59. package/src/utils/react-mdx-loader-plugin.ts +40 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * React HMR Strategy
3
+ *
4
+ * Handles hot module replacement for React components.
5
+ * Triggers module invalidation on changes to ensure fresh component re-renders.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import path from 'node:path';
11
+ import { pathToFileURL } from 'node:url';
12
+
13
+ import { HmrStrategy, HmrStrategyType, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
14
+ import { defaultBuildAdapter } from '@ecopages/core/build/build-adapter';
15
+ import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
16
+ import { FileNotFoundError, fileSystem } from '@ecopages/file-system';
17
+ import { Logger } from '@ecopages/logger';
18
+ import type { DefaultHmrContext, EcoComponentConfig } from '@ecopages/core';
19
+ import type { CompileOptions } from '@mdx-js/mdx';
20
+ import { injectHmrHandler } from './utils/hmr-scripts.ts';
21
+ import { createClientGraphBoundaryPlugin } from './utils/client-graph-boundary-plugin.ts';
22
+ import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from './utils/declared-modules.ts';
23
+ import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
24
+
25
+ const appLogger = new Logger('[ReactHmrStrategy]');
26
+
27
+ /**
28
+ * Strategy for handling React component HMR updates.
29
+ *
30
+ * This strategy provides React-specific HMR handling by rebuilding entrypoints
31
+ * and injecting HMR acceptance handlers that trigger module invalidation.
32
+ *
33
+ * The processing steps are:
34
+ * 1. Check if any React entrypoints are registered
35
+ * 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
39
+ *
40
+ * @remarks
41
+ * This strategy has higher priority than generic JsHmrStrategy, allowing it
42
+ * to handle React files specially while falling back to generic handling for
43
+ * non-React files.
44
+ *
45
+ * Future enhancement: Track dependencies using Bun's transpiler API to only
46
+ * rebuild affected entrypoints instead of all of them.
47
+ *
48
+ * @see https://bun.sh/docs/runtime/transpiler
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * const context = {
53
+ * getWatchedFiles: () => watchedFilesMap,
54
+ * getSpecifierMap: () => specifierMap,
55
+ * getDistDir: () => '/path/to/dist/_hmr',
56
+ * getPlugins: () => [],
57
+ * getSrcDir: () => '/path/to/src',
58
+ * getLayoutsDir: () => '/path/to/src/layouts'
59
+ * };
60
+ * const strategy = new ReactHmrStrategy(context);
61
+ * ```
62
+ */
63
+ export class ReactHmrStrategy extends HmrStrategy {
64
+ readonly type = HmrStrategyType.INTEGRATION;
65
+ private mdxCompilerOptions?: CompileOptions;
66
+ private readonly knownEntrypoints = new Set<string>();
67
+
68
+ private async importNodePageModule(entrypointPath: string): Promise<{
69
+ default?: { config?: EcoComponentConfig };
70
+ config?: EcoComponentConfig;
71
+ }> {
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
+ };
144
+ }
145
+
146
+ /**
147
+ * Creates a new React HMR strategy instance.
148
+ *
149
+ * @param context - The HMR context providing access to watched files, plugins, build directories,
150
+ * and the layouts directory for detecting layout file changes that require full
151
+ * page reloads instead of module-level HMR updates.
152
+ * @param pageMetadataCache - React-only cache of declared browser modules discovered during
153
+ * server rendering. This avoids re-importing unchanged page modules
154
+ * during save-time Fast Refresh rebuilds.
155
+ * @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
156
+ * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
157
+ * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
158
+ */
159
+ constructor(
160
+ private context: DefaultHmrContext,
161
+ private pageMetadataCache: ReactHmrPageMetadataCache,
162
+ mdxCompilerOptions?: CompileOptions,
163
+ private explicitGraphEnabled = false,
164
+ ) {
165
+ super();
166
+ this.mdxCompilerOptions = mdxCompilerOptions;
167
+ }
168
+
169
+ /**
170
+ * Returns build plugins for React HMR bundling.
171
+ *
172
+ * Includes the client graph boundary plugin to prevent undeclared imports
173
+ * (including `node:*`) from breaking the browser bundle.
174
+ */
175
+ 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
+ ];
185
+
186
+ return [
187
+ createClientGraphBoundaryPlugin({
188
+ absWorkingDir: path.dirname(this.context.getSrcDir()),
189
+ alwaysAllowSpecifiers: allowSpecifiers,
190
+ declaredModules,
191
+ }),
192
+ ...this.context.getPlugins(),
193
+ this.createUseSyncExternalStoreShimPlugin(),
194
+ ];
195
+ }
196
+
197
+ private isReactEntrypoint(filePath: string): boolean {
198
+ if (filePath.endsWith('.tsx')) {
199
+ return true;
200
+ }
201
+
202
+ return filePath.endsWith('.mdx') && this.mdxCompilerOptions !== undefined;
203
+ }
204
+
205
+ /**
206
+ * Determines if the file is a React/MDX entrypoint that's registered for HMR.
207
+ *
208
+ * @param filePath - Absolute path to the changed file
209
+ * @returns True if this is a registered React or MDX entrypoint
210
+ */
211
+ matches(filePath: string): boolean {
212
+ const watchedFiles = this.context.getWatchedFiles();
213
+ appLogger.debug(`Checking ${filePath}. Watched: ${watchedFiles.size}`);
214
+ if (watchedFiles.size === 0) {
215
+ return false;
216
+ }
217
+
218
+ return this.isReactEntrypoint(filePath);
219
+ }
220
+
221
+ /**
222
+ * Checks if a file is a layout file.
223
+ *
224
+ * Layout files require special HMR handling because they wrap multiple pages and affect
225
+ * the entire page structure. When a layout changes, we trigger a 'layout-update' event
226
+ * instead of a regular 'update' event, which instructs the browser to perform a full
227
+ * page reload (or clear cache and re-render) rather than attempting module-level HMR.
228
+ *
229
+ * @param filePath - Absolute path to the file
230
+ * @returns True if the file is in the layouts directory
231
+ */
232
+ private isLayoutFile(filePath: string): boolean {
233
+ return filePath.startsWith(this.context.getLayoutsDir());
234
+ }
235
+
236
+ /**
237
+ * Processes a React file change by rebuilding all React entrypoints.
238
+ *
239
+ * For layout files, broadcasts a 'layout-update' event to trigger full page reload.
240
+ * For regular components/pages, broadcasts 'update' events for module-level HMR.
241
+ * When a page entrypoint is first registered, only that entrypoint is built.
242
+ * Subsequent file updates rebuild all watched React entrypoints as usual.
243
+ *
244
+ * @param _filePath - Absolute path to the changed file
245
+ * @returns Action to broadcast update events (layout-update for layouts, update for components)
246
+ */
247
+ async process(_filePath: string): Promise<HmrAction> {
248
+ appLogger.debug(`Processing ${_filePath}`);
249
+ const watchedFiles = this.context.getWatchedFiles();
250
+
251
+ if (watchedFiles.size === 0) {
252
+ appLogger.debug(`No watched files`);
253
+ return { type: 'none' };
254
+ }
255
+
256
+ const isLayout = this.isLayoutFile(_filePath);
257
+ if (isLayout) {
258
+ appLogger.debug(`Detected layout file change: ${_filePath}`);
259
+ }
260
+
261
+ const entrypointsToBuild =
262
+ !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath)
263
+ ? [[_filePath, watchedFiles.get(_filePath)!]]
264
+ : watchedFiles.entries();
265
+ this.knownEntrypoints.add(_filePath);
266
+
267
+ const updates: string[] = [];
268
+ for (const [entrypoint, outputUrl] of entrypointsToBuild) {
269
+ if (!this.isReactEntrypoint(entrypoint)) {
270
+ continue;
271
+ }
272
+
273
+ appLogger.debug(`Bundling ${entrypoint}`);
274
+ const success = await this.bundleReactEntrypoint(entrypoint, outputUrl);
275
+ if (success) {
276
+ updates.push(outputUrl);
277
+ }
278
+ }
279
+
280
+ if (updates.length > 0) {
281
+ if (isLayout) {
282
+ appLogger.debug(`Layout update detected, sending layout-update event`);
283
+ return {
284
+ type: 'broadcast',
285
+ events: [
286
+ {
287
+ type: 'layout-update',
288
+ },
289
+ ],
290
+ };
291
+ }
292
+
293
+ appLogger.debug(`Broadcasting ${updates.length} updates`);
294
+ return {
295
+ type: 'broadcast',
296
+ events: updates.map((path) => ({
297
+ type: 'update',
298
+ path,
299
+ timestamp: Date.now(),
300
+ })),
301
+ };
302
+ }
303
+
304
+ appLogger.debug(`No updates generated`);
305
+ return { type: 'none' };
306
+ }
307
+
308
+ /**
309
+ * Bundles a single React/MDX entrypoint with HMR support.
310
+ *
311
+ * @param entrypointPath - Absolute path to the source file
312
+ * @param outputUrl - URL path for the bundled file
313
+ * @returns True if bundling was successful
314
+ */
315
+ private async bundleReactEntrypoint(entrypointPath: string, outputUrl: string): Promise<boolean> {
316
+ try {
317
+ const isMdx = entrypointPath.endsWith('.mdx');
318
+ const srcDir = this.context.getSrcDir();
319
+ const relativePath = path.relative(srcDir, entrypointPath);
320
+ const relativePathJs = relativePath.replace(/\.(tsx?|jsx?|mdx)$/, '.js');
321
+ const encodedPathJs = this.encodeDynamicSegments(relativePathJs);
322
+ const outputPath = path.join(this.context.getDistDir(), encodedPathJs);
323
+ const tempDir = path.dirname(outputPath);
324
+
325
+ const declaredModules = this.pageMetadataCache.getDeclaredModules(entrypointPath)
326
+ ? this.pageMetadataCache.getDeclaredModules(entrypointPath)!
327
+ : isMdx
328
+ ? await collectPageDeclaredModules(entrypointPath)
329
+ : collectPageDeclaredModulesFromModule(await this.importNodePageModule(entrypointPath));
330
+ const plugins = this.getBuildPlugins(declaredModules);
331
+
332
+ if (isMdx && this.mdxCompilerOptions) {
333
+ const { createReactMdxLoaderPlugin } = await import('./utils/react-mdx-loader-plugin.ts');
334
+ const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
335
+ plugins.unshift(mdxPlugin);
336
+ }
337
+
338
+ const result = await defaultBuildAdapter.build({
339
+ entrypoints: [entrypointPath],
340
+ outdir: tempDir,
341
+ naming: `[name].[hash].tmp`,
342
+ target: 'browser',
343
+ format: 'esm',
344
+ sourcemap: 'none',
345
+ plugins,
346
+ minify: false,
347
+ external: ['react', 'react-dom', 'react/jsx-runtime', 'react/jsx-dev-runtime', 'react-dom/client'],
348
+ });
349
+
350
+ if (!result.success) {
351
+ appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
352
+ return false;
353
+ }
354
+
355
+ const tempFile = result.outputs[0]?.path;
356
+ if (!tempFile) {
357
+ appLogger.error(`No output file generated for ${entrypointPath}`);
358
+ return false;
359
+ }
360
+
361
+ const processed = await this.processOutput(tempFile, outputPath, outputUrl);
362
+ return processed;
363
+ } catch (error) {
364
+ appLogger.error(`Error bundling ${entrypointPath}:`, error as Error);
365
+ return false;
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Encodes dynamic route segments (brackets) in file paths.
371
+ * Converts `[slug]` to `_slug_` to avoid filesystem issues.
372
+ */
373
+ private encodeDynamicSegments(filepath: string): string {
374
+ return filepath.replace(/\[([^\]]+)\]/g, '_$1_');
375
+ }
376
+
377
+ /**
378
+ * Processes bundled output by replacing specifiers and injecting HMR handler.
379
+ * Writes to temp file first, then renames atomically to avoid conflicts.
380
+ *
381
+ * @param tempPath - Path to the temporary bundled file
382
+ * @param finalPath - Final destination path
383
+ * @param url - URL path for logging
384
+ * @returns True if processing was successful
385
+ */
386
+ private async processOutput(tempPath: string, finalPath: string, url: string): Promise<boolean> {
387
+ if (!fileSystem.exists(tempPath)) {
388
+ appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
389
+ return false;
390
+ }
391
+
392
+ try {
393
+ let code = await fileSystem.readFile(tempPath);
394
+
395
+ code = this.replaceBareSpecifiers(code);
396
+ code = injectHmrHandler(code);
397
+
398
+ await fileSystem.writeAsync(finalPath, code);
399
+ await fileSystem.removeAsync(tempPath).catch(() => {});
400
+
401
+ appLogger.debug(`Processed ${url} with HMR handler`);
402
+ return true;
403
+ } catch (error) {
404
+ if (
405
+ error instanceof FileNotFoundError ||
406
+ (error instanceof Error && error.message.includes('not found')) ||
407
+ (error instanceof Error && 'code' in error && error.code === 'ENOENT')
408
+ ) {
409
+ appLogger.debug(`Skipping stale temp output for ${url}: ${tempPath}`);
410
+ await fileSystem.removeAsync(tempPath).catch(() => {});
411
+ return false;
412
+ }
413
+
414
+ appLogger.error(`Error processing output for ${url}:`, error as Error);
415
+ await fileSystem.removeAsync(tempPath).catch(() => {});
416
+ return false;
417
+ }
418
+ }
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
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * This module contains the React renderer
3
+ * @module
4
+ */
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoComponentConfig, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
6
+ import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
7
+ import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
8
+ import { type ReactNode } from 'react';
9
+ import type { CompileOptions } from '@mdx-js/mdx';
10
+ import type { ReactRouterAdapter } from './router-adapter.js';
11
+ import { ReactBundleService } from './services/react-bundle.service.js';
12
+ import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
13
+ import { ReactPageModuleService } from './services/react-page-module.service.js';
14
+ import { ReactHydrationAssetService } from './services/react-hydration-asset.service.js';
15
+ /**
16
+ * Error thrown when an error occurs while rendering a React component.
17
+ */
18
+ export declare class ReactRenderError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ /**
22
+ * Error thrown when an error occurs while bundling a React component.
23
+ */
24
+ export declare class BundleError extends Error {
25
+ readonly logs: string[];
26
+ constructor(message: string, logs: string[]);
27
+ }
28
+ /**
29
+ * Renderer for React components.
30
+ * @extends IntegrationRenderer
31
+ */
32
+ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
33
+ name: string;
34
+ componentDirectory: string;
35
+ private componentRenderSequence;
36
+ static routerAdapter: ReactRouterAdapter | undefined;
37
+ static mdxCompilerOptions: CompileOptions | undefined;
38
+ static mdxExtensions: string[];
39
+ static hmrPageMetadataCache: ReactHmrPageMetadataCache | undefined;
40
+ /**
41
+ * Enables explicit graph behavior for React page-entry bundling.
42
+ *
43
+ * When true, page-entry bundles disable AST server-only stripping and rely
44
+ * on explicit dependency declarations for browser graph composition.
45
+ */
46
+ static explicitGraphEnabled: boolean;
47
+ /** @internal */
48
+ readonly bundleService: ReactBundleService;
49
+ /** @internal */
50
+ readonly pageModuleService: ReactPageModuleService;
51
+ /** @internal */
52
+ readonly hydrationAssetService: ReactHydrationAssetService;
53
+ constructor(options: {
54
+ appConfig: ConstructorParameters<typeof IntegrationRenderer>[0]['appConfig'];
55
+ assetProcessingService: ConstructorParameters<typeof IntegrationRenderer>[0]['assetProcessingService'];
56
+ resolvedIntegrationDependencies?: ProcessedAsset[];
57
+ runtimeOrigin: string;
58
+ });
59
+ protected shouldRenderPageComponent(): boolean;
60
+ /**
61
+ * Renders a React component for component-level orchestration.
62
+ *
63
+ * Behavior:
64
+ * - 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.
68
+ *
69
+ * This preserves DOM shape for global CSS/layout selectors while keeping a
70
+ * deterministic mount target per component instance.
71
+ */
72
+ renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
73
+ /**
74
+ * Checks if the given file path corresponds to an MDX file based on configured extensions.
75
+ * @param filePath - The file path to check
76
+ * @returns True if the file is an MDX file
77
+ */
78
+ isMdxFile(filePath: string): boolean;
79
+ /**
80
+ * Processes MDX-specific configuration dependencies including layout dependencies.
81
+ * @param pagePath - Absolute path to the MDX page file
82
+ * @returns Processed assets for MDX configuration dependencies
83
+ */
84
+ private processMdxConfigDependencies;
85
+ buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
86
+ protected importPageFile(file: string): Promise<EcoPageFile<{
87
+ config?: EcoComponentConfig;
88
+ }>>;
89
+ render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody>;
90
+ /**
91
+ * Safely extracts locals for client-side hydration.
92
+ *
93
+ * 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.
96
+ *
97
+ * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
98
+ * to prevent accidental use. This method safely detects that case and returns
99
+ * `undefined` instead of throwing.
100
+ *
101
+ * @param locals - The locals object from the render context
102
+ * @returns The locals object if serializable, undefined otherwise
103
+ */
104
+ private getSerializableLocals;
105
+ renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
106
+ }