@ecopages/react 0.2.0-alpha.2 → 0.2.0-alpha.21

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 (66) hide show
  1. package/CHANGELOG.md +19 -39
  2. package/README.md +160 -18
  3. package/package.json +6 -6
  4. package/src/react-hmr-strategy.d.ts +26 -21
  5. package/src/react-hmr-strategy.js +91 -110
  6. package/src/react-renderer.d.ts +165 -41
  7. package/src/react-renderer.js +451 -158
  8. package/src/react.constants.d.ts +1 -0
  9. package/src/react.constants.js +4 -0
  10. package/src/react.plugin.d.ts +37 -108
  11. package/src/react.plugin.js +125 -54
  12. package/src/react.types.d.ts +88 -0
  13. package/src/react.types.js +0 -0
  14. package/src/router-adapter.d.ts +2 -2
  15. package/src/services/react-bundle.service.d.ts +4 -25
  16. package/src/services/react-bundle.service.js +39 -91
  17. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  18. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  19. package/src/services/react-hydration-asset.service.d.ts +7 -6
  20. package/src/services/react-hydration-asset.service.js +29 -17
  21. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  22. package/src/services/react-mdx-config-dependency.service.js +122 -0
  23. package/src/services/react-page-module.service.d.ts +8 -2
  24. package/src/services/react-page-module.service.js +44 -37
  25. package/src/services/react-page-payload.service.d.ts +46 -0
  26. package/src/services/react-page-payload.service.js +67 -0
  27. package/src/services/react-runtime-bundle.service.d.ts +14 -12
  28. package/src/services/react-runtime-bundle.service.js +103 -180
  29. package/src/utils/client-graph-boundary-plugin.js +149 -11
  30. package/src/utils/component-config-traversal.d.ts +36 -0
  31. package/src/utils/component-config-traversal.js +54 -0
  32. package/src/utils/declared-modules.d.ts +1 -1
  33. package/src/utils/declared-modules.js +7 -16
  34. package/src/utils/dynamic.test.browser.d.ts +1 -0
  35. package/src/utils/dynamic.test.browser.js +33 -0
  36. package/src/utils/hydration-scripts.d.ts +19 -4
  37. package/src/utils/hydration-scripts.js +102 -39
  38. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  39. package/src/utils/hydration-scripts.test.browser.js +126 -0
  40. package/src/utils/reachability-analyzer.d.ts +12 -1
  41. package/src/utils/reachability-analyzer.js +101 -5
  42. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  43. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  44. package/src/utils/react-mdx-loader-plugin.js +13 -5
  45. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  46. package/src/utils/react-runtime-specifier-map.js +37 -0
  47. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  48. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  49. package/src/react-hmr-strategy.ts +0 -444
  50. package/src/react-renderer.ts +0 -403
  51. package/src/react.plugin.ts +0 -241
  52. package/src/router-adapter.ts +0 -95
  53. package/src/services/react-bundle.service.ts +0 -212
  54. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  55. package/src/services/react-hydration-asset.service.ts +0 -260
  56. package/src/services/react-page-module.service.ts +0 -214
  57. package/src/services/react-runtime-bundle.service.ts +0 -271
  58. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  59. package/src/utils/client-only.ts +0 -27
  60. package/src/utils/declared-modules.ts +0 -99
  61. package/src/utils/dynamic.ts +0 -27
  62. package/src/utils/hmr-scripts.ts +0 -47
  63. package/src/utils/html-boundary.ts +0 -66
  64. package/src/utils/hydration-scripts.ts +0 -338
  65. package/src/utils/reachability-analyzer.ts +0 -440
  66. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,14 +1,23 @@
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 { rewriteRuntimeSpecifierAliases } from "@ecopages/core/build/runtime-specifier-aliases";
4
+ import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
5
5
  import { FileNotFoundError, fileSystem } from "@ecopages/file-system";
6
6
  import { Logger } from "@ecopages/logger";
7
7
  import { injectHmrHandler } from "./utils/hmr-scripts.js";
8
8
  import { createClientGraphBoundaryPlugin } from "./utils/client-graph-boundary-plugin.js";
9
9
  import { collectPageDeclaredModules, collectPageDeclaredModulesFromModule } from "./utils/declared-modules.js";
10
+ import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-specifier-map.js";
11
+ import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
10
12
  const appLogger = new Logger("[ReactHmrStrategy]");
11
13
  class ReactHmrStrategy extends HmrStrategy {
14
+ type = HmrStrategyType.INTEGRATION;
15
+ mdxCompilerOptions;
16
+ ownedTemplateExtensions;
17
+ allTemplateExtensions;
18
+ async importNodePageModule(entrypointPath) {
19
+ return await this.context.importServerModule(entrypointPath);
20
+ }
12
21
  /**
13
22
  * Creates a new React HMR strategy instance.
14
23
  *
@@ -22,75 +31,17 @@ class ReactHmrStrategy extends HmrStrategy {
22
31
  * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
23
32
  * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
24
33
  */
25
- constructor(context, pageMetadataCache, mdxCompilerOptions, explicitGraphEnabled = false) {
34
+ context;
35
+ pageMetadataCache;
36
+ explicitGraphEnabled;
37
+ constructor(context, pageMetadataCache, mdxCompilerOptions, ownedTemplateExtensions = [".tsx"], allTemplateExtensions = [".tsx"], explicitGraphEnabled = false) {
26
38
  super();
27
39
  this.context = context;
28
40
  this.pageMetadataCache = pageMetadataCache;
29
41
  this.explicitGraphEnabled = explicitGraphEnabled;
30
42
  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
- };
43
+ this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
44
+ this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
94
45
  }
95
46
  /**
96
47
  * Returns build plugins for React HMR bundling.
@@ -99,30 +50,57 @@ class ReactHmrStrategy extends HmrStrategy {
99
50
  * (including `node:*`) from breaking the browser bundle.
100
51
  */
101
52
  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
- ];
53
+ const runtimeSpecifierMap = this.context.getSpecifierMap();
54
+ const allowSpecifiers = getReactClientGraphAllowSpecifiers(runtimeSpecifierMap.keys());
55
+ const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, {
56
+ name: "react-hmr-runtime-specifier-alias"
57
+ });
111
58
  return [
112
59
  createClientGraphBoundaryPlugin({
113
60
  absWorkingDir: path.dirname(this.context.getSrcDir()),
114
61
  alwaysAllowSpecifiers: allowSpecifiers,
115
62
  declaredModules
116
63
  }),
64
+ ...runtimeAliasPlugin ? [runtimeAliasPlugin] : [],
117
65
  ...this.context.getPlugins(),
118
- this.createUseSyncExternalStoreShimPlugin()
66
+ createUseSyncExternalStoreShimPlugin({
67
+ name: "react-hmr-use-sync-external-store-shim",
68
+ namespace: "ecopages-react-hmr-shim"
69
+ })
119
70
  ];
120
71
  }
121
72
  isReactEntrypoint(filePath) {
122
- if (filePath.endsWith(".tsx")) {
73
+ if (filePath.endsWith(".mdx")) {
74
+ return this.mdxCompilerOptions !== void 0;
75
+ }
76
+ if (!filePath.endsWith(".tsx")) {
77
+ return false;
78
+ }
79
+ if (!this.isRouteTemplate(filePath)) {
123
80
  return true;
124
81
  }
125
- return filePath.endsWith(".mdx") && this.mdxCompilerOptions !== void 0;
82
+ const templateExtension = this.resolveTemplateExtension(filePath);
83
+ if (!templateExtension) {
84
+ return false;
85
+ }
86
+ return this.ownedTemplateExtensions.has(templateExtension);
87
+ }
88
+ /**
89
+ * Returns true when a route file uses a compound extension like `page.foo.tsx`.
90
+ *
91
+ * @remarks
92
+ * React integration owns plain `.tsx` route templates. Compound extensions in
93
+ * pages/layouts are integration-specific route templates and should not be
94
+ * claimed by React HMR strategy.
95
+ */
96
+ isRouteTemplate(filePath) {
97
+ return filePath.startsWith(this.context.getPagesDir()) || filePath.startsWith(this.context.getLayoutsDir());
98
+ }
99
+ resolveTemplateExtension(filePath) {
100
+ return this.allTemplateExtensions.find((extension) => filePath.endsWith(extension));
101
+ }
102
+ ownsWatchedEntrypoint(filePath) {
103
+ return this.pageMetadataCache.ownsEntrypoint(filePath);
126
104
  }
127
105
  /**
128
106
  * Determines if the file is a React/MDX entrypoint that's registered for HMR.
@@ -136,6 +114,9 @@ class ReactHmrStrategy extends HmrStrategy {
136
114
  if (watchedFiles.size === 0) {
137
115
  return false;
138
116
  }
117
+ if (watchedFiles.has(filePath)) {
118
+ return this.ownsWatchedEntrypoint(filePath);
119
+ }
139
120
  return this.isReactEntrypoint(filePath);
140
121
  }
141
122
  /**
@@ -174,8 +155,12 @@ class ReactHmrStrategy extends HmrStrategy {
174
155
  if (isLayout) {
175
156
  appLogger.debug(`Detected layout file change: ${_filePath}`);
176
157
  }
177
- const entrypointsToBuild = !this.knownEntrypoints.has(_filePath) && watchedFiles.has(_filePath) ? [[_filePath, watchedFiles.get(_filePath)]] : watchedFiles.entries();
178
- this.knownEntrypoints.add(_filePath);
158
+ const changedEntrypointOutput = watchedFiles.get(_filePath);
159
+ if (changedEntrypointOutput && !this.ownsWatchedEntrypoint(_filePath)) {
160
+ appLogger.debug(`Skipping non-React watched entrypoint: ${_filePath}`);
161
+ return { type: "none" };
162
+ }
163
+ const entrypointsToBuild = changedEntrypointOutput ? [[_filePath, changedEntrypointOutput]] : watchedFiles.entries();
179
164
  const updates = [];
180
165
  for (const [entrypoint, outputUrl] of entrypointsToBuild) {
181
166
  if (!this.isReactEntrypoint(entrypoint)) {
@@ -235,16 +220,13 @@ class ReactHmrStrategy extends HmrStrategy {
235
220
  const mdxPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
236
221
  plugins.unshift(mdxPlugin);
237
222
  }
238
- const result = await defaultBuildAdapter.build({
223
+ const result = await this.context.getBrowserBundleService().bundle({
224
+ profile: "hmr-entrypoint",
239
225
  entrypoints: [entrypointPath],
240
226
  outdir: tempDir,
241
227
  naming: `[name].[hash].tmp`,
242
- target: "browser",
243
- format: "esm",
244
- sourcemap: "none",
245
228
  plugins,
246
- minify: false,
247
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"]
229
+ minify: false
248
230
  });
249
231
  if (!result.success) {
250
232
  appLogger.error(`Failed to build ${entrypointPath}:`, result.logs);
@@ -255,13 +237,33 @@ class ReactHmrStrategy extends HmrStrategy {
255
237
  appLogger.error(`No output file generated for ${entrypointPath}`);
256
238
  return false;
257
239
  }
258
- const processed = await this.processOutput(tempFile, outputPath, outputUrl);
240
+ const resolvedTempFile = await this.resolveTempOutputPath(tempFile);
241
+ if (!resolvedTempFile) {
242
+ appLogger.debug(`Skipping stale temp output for ${outputUrl}: ${tempFile}`);
243
+ return false;
244
+ }
245
+ const processed = await this.processOutput(resolvedTempFile, outputPath, outputUrl);
259
246
  return processed;
260
247
  } catch (error) {
261
248
  appLogger.error(`Error bundling ${entrypointPath}:`, error);
262
249
  return false;
263
250
  }
264
251
  }
252
+ async resolveTempOutputPath(tempPath) {
253
+ if (fileSystem.exists(tempPath)) {
254
+ return tempPath;
255
+ }
256
+ if (!tempPath.includes("[hash]")) {
257
+ return tempPath;
258
+ }
259
+ const directory = path.dirname(tempPath);
260
+ const pattern = path.basename(tempPath).replaceAll("[hash]", "*");
261
+ const matches = await fileSystem.glob([pattern], { cwd: directory });
262
+ if (matches.length === 0) {
263
+ return null;
264
+ }
265
+ return path.isAbsolute(matches[0]) ? matches[0] : path.join(directory, matches[0]);
266
+ }
265
267
  /**
266
268
  * Encodes dynamic route segments (brackets) in file paths.
267
269
  * Converts `[slug]` to `_slug_` to avoid filesystem issues.
@@ -270,7 +272,7 @@ class ReactHmrStrategy extends HmrStrategy {
270
272
  return filepath.replace(/\[([^\]]+)\]/g, "_$1_");
271
273
  }
272
274
  /**
273
- * Processes bundled output by replacing specifiers and injecting HMR handler.
275
+ * Processes bundled output and injects the React HMR handler.
274
276
  * Writes to temp file first, then renames atomically to avoid conflicts.
275
277
  *
276
278
  * @param tempPath - Path to the temporary bundled file
@@ -285,7 +287,7 @@ class ReactHmrStrategy extends HmrStrategy {
285
287
  }
286
288
  try {
287
289
  let code = await fileSystem.readFile(tempPath);
288
- code = this.replaceBareSpecifiers(code);
290
+ code = rewriteRuntimeSpecifierAliases(code, this.context.getSpecifierMap());
289
291
  code = injectHmrHandler(code);
290
292
  await fileSystem.writeAsync(finalPath, code);
291
293
  await fileSystem.removeAsync(tempPath).catch(() => {
@@ -305,27 +307,6 @@ class ReactHmrStrategy extends HmrStrategy {
305
307
  return false;
306
308
  }
307
309
  }
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
310
  }
330
311
  export {
331
312
  ReactHmrStrategy
@@ -2,16 +2,24 @@
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';
6
- import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
5
+ import type { ComponentRenderInput, ComponentRenderResult, EcoComponent, EcoPageFile, IntegrationRendererRenderOptions, RouteRendererBody } from '@ecopages/core';
6
+ import { IntegrationRenderer, type RenderToResponseContext, type RouteModuleLoadOptions } from '@ecopages/core/route-renderer/integration-renderer';
7
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';
8
+ import type { ReactNode } from 'react';
9
+ import type { ReactRendererConfig } from './react.types.js';
11
10
  import { ReactBundleService } from './services/react-bundle.service.js';
12
- import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
11
+ import { ReactMdxConfigDependencyService } from './services/react-mdx-config-dependency.service.js';
13
12
  import { ReactPageModuleService } from './services/react-page-module.service.js';
13
+ import { ReactPagePayloadService } from './services/react-page-payload.service.js';
14
14
  import { ReactHydrationAssetService } from './services/react-hydration-asset.service.js';
15
+ export type { ReactRendererConfig } from './react.types.js';
16
+ type ReactRuntimeModules = {
17
+ react: Pick<typeof import('react'), 'createElement' | 'Fragment'>;
18
+ reactDomServer: Pick<typeof import('react-dom/server'), 'renderToReadableStream' | 'renderToString'>;
19
+ };
20
+ export type ReactRendererOptions = ConstructorParameters<typeof IntegrationRenderer>[0] & {
21
+ reactConfig?: ReactRendererConfig;
22
+ };
15
23
  /**
16
24
  * Error thrown when an error occurs while rendering a React component.
17
25
  */
@@ -32,75 +40,191 @@ export declare class BundleError extends Error {
32
40
  export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
33
41
  name: string;
34
42
  componentDirectory: string;
35
- private componentRenderSequence;
36
- static routerAdapter: ReactRouterAdapter | undefined;
37
- static mdxCompilerOptions: CompileOptions | undefined;
38
- static mdxExtensions: string[];
39
- static hmrPageMetadataCache: ReactHmrPageMetadataCache | undefined;
43
+ private reactRuntimeModules?;
44
+ private readonly routerAdapter?;
45
+ private readonly mdxCompilerOptions?;
46
+ private readonly mdxExtensions;
47
+ private readonly hmrPageMetadataCache?;
40
48
  /**
41
49
  * Enables explicit graph behavior for React page-entry bundling.
42
50
  *
43
51
  * When true, page-entry bundles disable AST server-only stripping and rely
44
52
  * on explicit dependency declarations for browser graph composition.
45
53
  */
46
- static explicitGraphEnabled: boolean;
54
+ private readonly explicitGraphEnabled;
47
55
  /** @internal */
48
56
  readonly bundleService: ReactBundleService;
49
57
  /** @internal */
50
58
  readonly pageModuleService: ReactPageModuleService;
51
59
  /** @internal */
52
60
  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
- });
61
+ /** @internal */
62
+ readonly pagePayloadService: ReactPagePayloadService;
63
+ /** @internal */
64
+ readonly mdxConfigDependencyService: ReactMdxConfigDependencyService;
65
+ constructor(options: ReactRendererOptions);
59
66
  protected shouldRenderPageComponent(): boolean;
67
+ /**
68
+ * Reads the declared integration name for a component or layout.
69
+ *
70
+ * We honor both the explicit `config.integration` override and injected
71
+ * `config.__eco.integration` metadata because pages can arrive here through
72
+ * authored config as well as build-time component metadata.
73
+ */
74
+ private getComponentIntegration;
75
+ /**
76
+ * Returns whether a component should stay inside the React render lane.
77
+ *
78
+ * Components without explicit integration metadata are treated as React-owned
79
+ * here because this renderer only receives them after the route pipeline has
80
+ * already selected the React integration.
81
+ */
82
+ private isReactManagedComponent;
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
+ protected resolveReactRuntimeModules(): ReactRuntimeModules;
102
+ private getReactRuntimeModules;
103
+ /**
104
+ * Appends route hydration assets for a concrete page/view file to the current
105
+ * HTML transformer state.
106
+ */
107
+ private appendHydrationAssetsForFile;
108
+ /**
109
+ * Renders a non-React layout or HTML template and enforces that mixed shells
110
+ * return serialized HTML.
111
+ *
112
+ * The React renderer can compose through another integration's shell, but only
113
+ * if that shell yields a string that can be inserted into the final document.
114
+ */
115
+ private renderNonReactShellComponent;
116
+ /**
117
+ * Renders one React component boundary while preserving already-resolved child HTML.
118
+ *
119
+ * When nested boundary resolution has already produced child HTML for this
120
+ * boundary, the child payload must remain raw SSR output rather than a React
121
+ * string child, otherwise React would escape it. This helper renders a unique
122
+ * token through React and swaps that token back to the resolved HTML
123
+ * afterward.
124
+ *
125
+ * @param input Component render input for the current boundary.
126
+ * @param context React-specific render context for stable token generation.
127
+ * @returns Serialized component HTML with resolved child markup preserved.
128
+ */
129
+ private renderComponentHtml;
130
+ /**
131
+ * Restores raw child HTML that was temporarily replaced by a token during React SSR.
132
+ *
133
+ * Queued boundary resolution may render children through a fragment path before all
134
+ * nested integration tokens are resolved. When that happens, React must never see
135
+ * the resolved child HTML as a normal string child or it would escape it. The
136
+ * runtime context stores the placeholder token and the raw child HTML so the
137
+ * fragment render path can reinsert it before foreign boundary tokens are handled.
138
+ */
139
+ private restoreRuntimeChildHtml;
140
+ /**
141
+ * Renders queued child content through React and then resolves nested boundary tokens.
142
+ *
143
+ * This path is only used for children that were deferred while React rendered the
144
+ * parent boundary. It first restores any raw child HTML placeholders owned by the
145
+ * current runtime context, then asks the shared queued-boundary resolver to swap
146
+ * foreign integration tokens with their resolved HTML.
147
+ */
148
+ private renderQueuedChildrenToHtml;
149
+ /**
150
+ * Resolves queued renderer-owned boundary tokens produced during React component rendering.
151
+ *
152
+ * React components can enqueue nested boundaries while the parent HTML is being
153
+ * rendered. This delegates to the shared renderer-owned queue resolver but keeps
154
+ * the React-specific child rendering behavior local so raw child HTML and React's
155
+ * fragment rendering semantics stay coordinated.
156
+ */
157
+ private resolveQueuedBoundaryHtml;
158
+ private buildHydrationProps;
159
+ /**
160
+ * Builds the extra document props needed when React renders through a non-React HTML shell.
161
+ *
162
+ * Router-backed React pages still need to publish the canonical page-data script
163
+ * even when the outer document shell belongs to another integration.
164
+ */
165
+ private buildNonReactDocumentProps;
166
+ /**
167
+ * Renders a foreign integration component boundary that participates in React composition.
168
+ *
169
+ * Non-React components must resolve to serialized HTML so React can embed them as
170
+ * mixed-shell boundaries. Any component-owned dependencies still need to flow
171
+ * through the shared dependency resolver before queued boundary tokens are finalized.
172
+ */
173
+ private renderForeignComponentBoundary;
174
+ /**
175
+ * Renders a React-owned component boundary and attaches island hydration metadata when possible.
176
+ *
177
+ * This path keeps React-owned SSR, queued boundary resolution, and optional
178
+ * island hydration wiring together so the public `renderComponent()` method can
179
+ * read as orchestration rather than implementation detail.
180
+ */
181
+ private renderReactComponentBoundary;
60
182
  /**
61
183
  * Renders a React component for component-level orchestration.
62
184
  *
63
185
  * Behavior:
64
186
  * - 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.
187
+ * - When an explicit component instance id is provided, a stable
188
+ * `data-eco-component-id` attribute is attached so island hydration can target it.
189
+ * - Without an explicit instance id, component renders remain plain SSR output.
190
+ * - When resolved child HTML is provided, that boundary is treated as a pure SSR
191
+ * composition step and does not emit hydration assets for the parent wrapper.
68
192
  *
69
193
  * This preserves DOM shape for global CSS/layout selectors while keeping a
70
194
  * deterministic mount target per component instance.
71
195
  */
72
196
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
197
+ protected createComponentBoundaryRuntime(options: {
198
+ boundaryInput: ComponentRenderInput;
199
+ rendererCache: Map<string, IntegrationRenderer<any>>;
200
+ }): import("@ecopages/core").ComponentBoundaryRuntime;
73
201
  /**
74
202
  * Checks if the given file path corresponds to an MDX file based on configured extensions.
75
203
  * @param filePath - The file path to check
76
204
  * @returns True if the file is an MDX file
77
205
  */
78
206
  isMdxFile(filePath: string): boolean;
207
+ protected usesIntegrationPageImporter(file: string): boolean;
208
+ protected importIntegrationPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
209
+ protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
210
+ buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
79
211
  /**
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
212
+ * Renders a full route response for the filesystem page pipeline.
213
+ *
214
+ * This path receives already-resolved route metadata, layout, locals, and HTML
215
+ * template instances from the shared renderer orchestration. Its main job is to
216
+ * serialize only the browser-safe page payload, compose the mixed React/non-
217
+ * React shell tree, and hand the result back as a document body.
83
218
  */
84
- private processMdxConfigDependencies;
85
- buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
86
- protected importPageFile(file: string): Promise<EcoPageFile<{
87
- config?: EcoComponentConfig;
88
- }>>;
89
219
  render({ params, query, props, locals, pageLocals, metadata, Page, Layout, HtmlTemplate, pageProps, }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody>;
220
+ protected getDocumentAttributes(): Record<string, string> | undefined;
90
221
  /**
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.
222
+ * Renders an arbitrary React view through the application's HTML shell.
100
223
  *
101
- * @param locals - The locals object from the render context
102
- * @returns The locals object if serializable, undefined otherwise
224
+ * Unlike route rendering, this path starts from a single component rather than a
225
+ * page module discovered by the router. It still needs to resolve metadata,
226
+ * layout dependencies, and hydration assets so direct `ctx.render()` calls match
227
+ * normal page responses.
103
228
  */
104
- private getSerializableLocals;
105
229
  renderToResponse<P = Record<string, unknown>>(view: EcoComponent<P>, props: P, ctx: RenderToResponseContext): Promise<Response>;
106
230
  }