@ecopages/react 0.2.0-alpha.25 → 0.2.0-alpha.27

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.
package/CHANGELOG.md CHANGED
@@ -10,11 +10,11 @@ All notable changes to `@ecopages/react` are documented here.
10
10
 
11
11
  - Fixed router-managed React HMR page entries to reload the active route with a cleared persisted-layout cache so shared layout edits apply while the current page stays mounted.
12
12
  - Fixed router-managed React HMR handlers to forward the active page HMR entry when reloading the current route through React Router.
13
- - Fixed React route hydration bundles to resolve the router through the published import-map key and keep rerun navigation on the shared runtime graph.
13
+ - Fixed production React route hydration bundles to inline React runtime dependencies and import the router through the emitted page browser graph instead of a published import-map key.
14
14
  - Removed the redundant React page props bootstrap script so route hydration relies on the canonical `__ECO_PAGE_DATA__` payload.
15
- - Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer boundary resolution across Bun, Vite, and Nitro flows.
15
+ - Fixed React hydration, Fast Refresh, module loading, doctype handling, island asset reuse, and mixed-renderer foreign-subtree resolution across Bun, Vite, and Nitro flows.
16
16
  - Restored direct `ReactPlugin` construction so the exported class still accepts the public plugin options shape.
17
- - Fixed React boundary payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
17
+ - Fixed React foreign-subtree payload compatibility coverage and removed the plugin/renderer integration-name import cycle.
18
18
 
19
19
  ### Features
20
20
 
@@ -23,6 +23,9 @@ All notable changes to `@ecopages/react` are documented here.
23
23
  ### Refactoring
24
24
 
25
25
  - Collapsed React route hydration into one page-owned entry module that re-exports the page component and bundles runtime dependencies in production.
26
+ - Removed the router adapter `importMapKey` contract so both development and production route hydration follow the router bundle import path instead of split import-map and bundle-path models.
27
+ - Replaced the positional `ReactHmrStrategy` constructor with an options object so React HMR wiring can evolve without argument-order churn.
28
+ - Renamed the remaining React runtime alias internals away from `specifierMap` terminology now that import-map-era core seams are gone.
26
29
  - Consolidated React bundling, hydration, and runtime state behind shared service boundaries and `window.__ECO_PAGES__`.
27
30
  - Moved React plugin option/default resolution into the factory and replaced renderer static config with instance-owned runtime wiring.
28
31
  - Extracted React page-payload and locals serialization into a dedicated service to keep the renderer focused on orchestration.
@@ -34,7 +37,7 @@ All notable changes to `@ecopages/react` are documented here.
34
37
 
35
38
  - Added Vitest browser coverage for the React `dynamic()` utility using React Testing Library.
36
39
  - Added browser execution coverage for the generated React hydration bootstrap, including router ownership registration and page-root cleanup.
37
- - Added renderer-level coverage for the boundary payload compatibility contract, including non-attachable fragment roots.
40
+ - Added renderer-level coverage for the foreign-subtree payload compatibility contract, including non-attachable fragment roots.
38
41
 
39
42
  ### Documentation
40
43
 
package/README.md CHANGED
@@ -5,7 +5,8 @@ First-class integration for [React 19](https://react.dev/) in Ecopages. This plu
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- bun add @ecopages/react
8
+ bun add @ecopages/react react react-dom
9
+ bun add -d @types/react @types/react-dom
9
10
  ```
10
11
 
11
12
  ## Usage
@@ -26,15 +27,15 @@ export default config;
26
27
 
27
28
  ## Component-Level Islands
28
29
 
29
- By default, Ecopages React acts in island mode:
30
+ For component-level islands, Ecopages React uses this contract:
30
31
 
31
32
  - SSR output preserves the authored DOM structure (no unnecessary wrapper elements).
32
33
  - A stable `data-eco-component-id` attribute is attached to the component SSR root.
33
- - The client bootstrap mounts the component via `createRoot()` strictly within that root boundary.
34
+ - The island runtime replaces the SSR host with a dedicated client-owned container and mounts it with `createRoot()`. Full-page hydration paths use `hydrateRoot()`.
34
35
 
35
36
  > [!TIP]
36
37
  > **Full React SPA Routing:**
37
- > If you are building full React pages and want client-side navigation (SPA), use [@ecopages/react-router](../react-router/README.md) and pass it to the react plugin: `reactPlugin({ router: ecoRouter() })`.
38
+ > If you are building full React pages and want client-side navigation (SPA), use [@ecopages/react-router](../../react-router/README.md) and pass it to the react plugin: `reactPlugin({ router: ecoRouter() })`.
38
39
 
39
40
  ## MDX Support
40
41
 
@@ -65,15 +66,15 @@ export default config;
65
66
  The React integration can participate in mixed-renderer apps in three ways:
66
67
 
67
68
  - React can own the page or view directly.
68
- - React can render nested component boundaries inside pages owned by another integration.
69
+ - React can render nested foreign subtrees inside pages owned by another integration.
69
70
  - React can render through non-React page, layout, or document shells when those shell components return strings.
70
71
 
71
- When a non-React render pass enters a React-owned boundary, Ecopages hands that boundary back to the React renderer. When React renders through a non-React shell, that shell must serialize to HTML so React can insert the result into the final response without escaping it.
72
+ When a non-React render pass reaches a React-owned foreign child, Ecopages hands that foreign subtree back to the React renderer. When React renders through a non-React shell, that shell must serialize to HTML so React can insert the result into the final response without escaping it.
72
73
 
73
74
  Important:
74
75
 
75
76
  - Components that may render foreign children must declare those children in `config.dependencies.components`.
76
- - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not infer every foreign boundary from rendered HTML alone.
77
+ - Ecopages validates mixed-renderer ownership from declared dependencies during render preparation. It does not infer every foreign subtree from rendered HTML alone.
77
78
  - React still keeps its own child transport and hydration rules for React-owned subtrees.
78
79
 
79
80
  ## Server and Client Graph Contract
@@ -115,7 +116,7 @@ The client bundle keeps:
115
116
  - The page component render path.
116
117
  - Client-safe component dependencies reachable from render.
117
118
  - Layout wiring needed for hydration.
118
- - Router runtime state needed by [@ecopages/react-router](../react-router/README.md) when SPA mode is enabled.
119
+ - Router runtime state needed by [@ecopages/react-router](../../react-router/README.md) when SPA mode is enabled.
119
120
 
120
121
  The client bundle removes or excludes:
121
122
 
@@ -130,7 +131,7 @@ Important:
130
131
 
131
132
  ### AST Pipeline Order
132
133
 
133
- The browser-bound transform in [packages/integrations/react/src/utils/client-graph-boundary-plugin.ts](packages/integrations/react/src/utils/client-graph-boundary-plugin.ts) follows this order:
134
+ The browser-bound transform in [src/utils/client-graph-boundary-plugin.ts](src/utils/client-graph-boundary-plugin.ts) follows this order:
134
135
 
135
136
  1. Parse the module and build a reachability view of the client render graph.
136
137
  2. Remove imports that are not allowed or not reachable from the client graph.
@@ -165,7 +166,7 @@ The fix is to strip server-only `eco.page(...)` options after import pruning, wh
165
166
 
166
167
  The browser must not receive arbitrary request-scoped data.
167
168
 
168
- The React renderer in [packages/integrations/react/src/react-renderer.ts](packages/integrations/react/src/react-renderer.ts) serializes only the top-level `locals` keys explicitly declared by `Page.requires`. If a page does not declare `requires`, no `locals` are serialized for hydration.
169
+ The React renderer in [src/react-renderer.ts](src/react-renderer.ts) serializes only the top-level `locals` keys explicitly declared by `Page.requires`. If a page does not declare `requires`, no `locals` are serialized for hydration.
169
170
 
170
171
  Example:
171
172
 
@@ -190,8 +191,8 @@ Hydration must rebuild the same tree the server rendered.
190
191
 
191
192
  That applies to both:
192
193
 
193
- - non-router hydration scripts in [packages/integrations/react/src/utils/hydration-scripts.ts](packages/integrations/react/src/utils/hydration-scripts.ts)
194
- - router-backed hydration in [packages/react-router/src/router.ts](packages/react-router/src/router.ts)
194
+ - non-router hydration scripts in [src/utils/hydration-scripts.ts](src/utils/hydration-scripts.ts)
195
+ - router-backed hydration in [../../react-router/src/router.ts](../../react-router/src/router.ts)
195
196
 
196
197
  If the page render receives `locals` on the server and the layout also depends on those values, the client must pass the same serialized `locals` into the layout during hydration. Otherwise React will detect a mismatch.
197
198
 
@@ -199,9 +200,9 @@ If the page render receives `locals` on the server and the layout also depends o
199
200
 
200
201
  The main regression coverage lives in:
201
202
 
202
- - [packages/integrations/react/src/utils/client-graph-boundary-plugin.test.ts](packages/integrations/react/src/utils/client-graph-boundary-plugin.test.ts): verifies server-only `eco.page(...)` options are stripped from browser bundles.
203
- - [packages/integrations/react/src/react-renderer.locals.test.ts](packages/integrations/react/src/react-renderer.locals.test.ts): verifies only declared `requires` keys are serialized into hydration payloads.
204
- - [packages/integrations/react/src/utils/hydration-scripts.test.ts](packages/integrations/react/src/utils/hydration-scripts.test.ts): verifies non-router hydration passes serialized `locals` into layouts.
205
- - [packages/react-router/test/hmr-reload.test.browser.ts](packages/react-router/test/hmr-reload.test.browser.ts): verifies router-backed layout hydration receives `locals` with `persistLayouts` both enabled and disabled.
203
+ - [src/utils/client-graph-boundary-plugin.test.ts](src/utils/client-graph-boundary-plugin.test.ts): verifies server-only `eco.page(...)` options are stripped from browser bundles.
204
+ - [src/react-renderer.locals.test.ts](src/react-renderer.locals.test.ts): verifies only declared `requires` keys are serialized into hydration payloads.
205
+ - [src/utils/hydration-scripts.test.ts](src/utils/hydration-scripts.test.ts): verifies non-router hydration passes serialized `locals` into layouts.
206
+ - [../../react-router/test/hmr-reload.test.browser.ts](../../react-router/test/hmr-reload.test.browser.ts): verifies router-backed layout hydration receives `locals` with `persistLayouts` both enabled and disabled.
206
207
 
207
208
  If you change the AST transform or hydration flow, update the corresponding tests in the same change.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecopages/react",
3
- "version": "0.2.0-alpha.25",
3
+ "version": "0.2.0-alpha.27",
4
4
  "description": "React integration for Ecopages",
5
5
  "keywords": [
6
6
  "ecopages",
@@ -53,14 +53,14 @@
53
53
  "directory": "packages/integrations/react"
54
54
  },
55
55
  "peerDependencies": {
56
- "@ecopages/core": "0.2.0-alpha.25",
56
+ "@ecopages/core": "0.2.0-alpha.27",
57
57
  "@types/react": "^19",
58
58
  "@types/react-dom": "^19",
59
59
  "react": "^19",
60
60
  "react-dom": "^19"
61
61
  },
62
62
  "dependencies": {
63
- "@ecopages/file-system": "0.2.0-alpha.25",
63
+ "@ecopages/file-system": "0.2.0-alpha.27",
64
64
  "@ecopages/logger": "^0.2.3",
65
65
  "@mdx-js/esbuild": "^3.0.1",
66
66
  "@mdx-js/mdx": "^3.1.0",
@@ -10,6 +10,15 @@ import { HmrStrategy, type HmrAction } from '@ecopages/core/hmr/hmr-strategy';
10
10
  import type { DefaultHmrContext } from '@ecopages/core';
11
11
  import type { CompileOptions } from '@mdx-js/mdx';
12
12
  import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
13
+ export interface ReactHmrStrategyOptions {
14
+ context: DefaultHmrContext;
15
+ pageMetadataCache: ReactHmrPageMetadataCache;
16
+ runtimeAliasMap: ReadonlyMap<string, string>;
17
+ mdxCompilerOptions?: CompileOptions;
18
+ ownedTemplateExtensions?: string[];
19
+ allTemplateExtensions?: string[];
20
+ explicitGraphEnabled?: boolean;
21
+ }
13
22
  /**
14
23
  * Strategy for handling React component HMR updates.
15
24
  *
@@ -39,13 +48,16 @@ import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metada
39
48
  * ```typescript
40
49
  * const context = {
41
50
  * getWatchedFiles: () => watchedFilesMap,
42
- * getSpecifierMap: () => specifierMap,
43
51
  * getDistDir: () => '/path/to/dist/_hmr',
44
52
  * getPlugins: () => [],
45
53
  * getSrcDir: () => '/path/to/src',
46
54
  * getLayoutsDir: () => '/path/to/src/layouts'
47
55
  * };
48
- * const strategy = new ReactHmrStrategy(context);
56
+ * const strategy = new ReactHmrStrategy({
57
+ * context,
58
+ * pageMetadataCache,
59
+ * runtimeAliasMap
60
+ * });
49
61
  * ```
50
62
  */
51
63
  export declare class ReactHmrStrategy extends HmrStrategy {
@@ -57,20 +69,13 @@ export declare class ReactHmrStrategy extends HmrStrategy {
57
69
  /**
58
70
  * Creates a new React HMR strategy instance.
59
71
  *
60
- * @param context - The HMR context providing access to watched files, plugins, build directories,
61
- * and the layouts directory for detecting layout file changes that require full
62
- * page reloads instead of module-level HMR updates.
63
- * @param pageMetadataCache - React-only cache of declared browser modules discovered during
64
- * server rendering. This avoids re-importing unchanged page modules
65
- * during save-time Fast Refresh rebuilds.
66
- * @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
67
- * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
68
- * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
72
+ * @param options - React HMR runtime services and behavior flags.
69
73
  */
70
74
  private context;
71
75
  private pageMetadataCache;
72
76
  private explicitGraphEnabled;
73
- constructor(context: DefaultHmrContext, pageMetadataCache: ReactHmrPageMetadataCache, mdxCompilerOptions?: CompileOptions, ownedTemplateExtensions?: string[], allTemplateExtensions?: string[], explicitGraphEnabled?: boolean);
77
+ private readonly runtimeAliasMap;
78
+ constructor(options: ReactHmrStrategyOptions);
74
79
  /**
75
80
  * Returns build plugins for React HMR bundling.
76
81
  *
@@ -7,7 +7,7 @@ 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";
10
+ import { getReactClientGraphAllowSpecifiers } from "./utils/react-runtime-alias-map.js";
11
11
  import { createUseSyncExternalStoreShimPlugin } from "./utils/use-sync-external-store-shim-plugin.js";
12
12
  const appLogger = new Logger("[ReactHmrStrategy]");
13
13
  class ReactHmrStrategy extends HmrStrategy {
@@ -21,27 +21,23 @@ class ReactHmrStrategy extends HmrStrategy {
21
21
  /**
22
22
  * Creates a new React HMR strategy instance.
23
23
  *
24
- * @param context - The HMR context providing access to watched files, plugins, build directories,
25
- * and the layouts directory for detecting layout file changes that require full
26
- * page reloads instead of module-level HMR updates.
27
- * @param pageMetadataCache - React-only cache of declared browser modules discovered during
28
- * server rendering. This avoids re-importing unchanged page modules
29
- * during save-time Fast Refresh rebuilds.
30
- * @param mdxCompilerOptions - Optional MDX compiler options for processing .mdx files
31
- * @param explicitGraphEnabled - Enables explicit graph mode for React HMR bundling.
32
- * In explicit mode, HMR builds omit AST server-only stripping plugins in React paths.
24
+ * @param options - React HMR runtime services and behavior flags.
33
25
  */
34
26
  context;
35
27
  pageMetadataCache;
36
28
  explicitGraphEnabled;
37
- constructor(context, pageMetadataCache, mdxCompilerOptions, ownedTemplateExtensions = [".tsx"], allTemplateExtensions = [".tsx"], explicitGraphEnabled = false) {
29
+ runtimeAliasMap;
30
+ constructor(options) {
38
31
  super();
39
- this.context = context;
40
- this.pageMetadataCache = pageMetadataCache;
41
- this.explicitGraphEnabled = explicitGraphEnabled;
42
- this.mdxCompilerOptions = mdxCompilerOptions;
43
- this.ownedTemplateExtensions = new Set(ownedTemplateExtensions);
44
- this.allTemplateExtensions = [...allTemplateExtensions].sort((a, b) => b.length - a.length);
32
+ this.context = options.context;
33
+ this.pageMetadataCache = options.pageMetadataCache;
34
+ this.runtimeAliasMap = options.runtimeAliasMap;
35
+ this.explicitGraphEnabled = options.explicitGraphEnabled ?? false;
36
+ this.mdxCompilerOptions = options.mdxCompilerOptions;
37
+ this.ownedTemplateExtensions = new Set(options.ownedTemplateExtensions ?? [".tsx"]);
38
+ this.allTemplateExtensions = [...options.allTemplateExtensions ?? [".tsx"]].sort(
39
+ (a, b) => b.length - a.length
40
+ );
45
41
  }
46
42
  /**
47
43
  * Returns build plugins for React HMR bundling.
@@ -50,9 +46,8 @@ class ReactHmrStrategy extends HmrStrategy {
50
46
  * (including `node:*`) from breaking the browser bundle.
51
47
  */
52
48
  getBuildPlugins(declaredModules) {
53
- const runtimeSpecifierMap = this.context.getSpecifierMap();
54
- const allowSpecifiers = getReactClientGraphAllowSpecifiers(runtimeSpecifierMap.keys());
55
- const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, {
49
+ const allowSpecifiers = getReactClientGraphAllowSpecifiers(this.runtimeAliasMap.keys());
50
+ const runtimeAliasPlugin = createRuntimeSpecifierAliasPlugin(this.runtimeAliasMap, {
56
51
  name: "react-hmr-runtime-specifier-alias"
57
52
  });
58
53
  return [
@@ -287,7 +282,7 @@ class ReactHmrStrategy extends HmrStrategy {
287
282
  }
288
283
  try {
289
284
  let code = await fileSystem.readFile(tempPath);
290
- code = rewriteRuntimeSpecifierAliases(code, this.context.getSpecifierMap());
285
+ code = rewriteRuntimeSpecifierAliases(code, this.runtimeAliasMap);
291
286
  code = injectHmrHandler(code);
292
287
  await fileSystem.writeAsync(finalPath, code);
293
288
  await fileSystem.removeAsync(tempPath).catch(() => {
@@ -115,15 +115,15 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
115
115
  */
116
116
  private renderNonReactShellComponent;
117
117
  /**
118
- * Renders one React component boundary while preserving already-resolved child HTML.
118
+ * Renders one React component while preserving already-resolved child HTML.
119
119
  *
120
- * When nested boundary resolution has already produced child HTML for this
121
- * boundary, the child payload must remain raw SSR output rather than a React
120
+ * When nested foreign-subtree resolution has already produced child HTML for this
121
+ * component, the child payload must remain raw SSR output rather than a React
122
122
  * string child, otherwise React would escape it. This helper renders a unique
123
123
  * token through React and swaps that token back to the resolved HTML
124
124
  * afterward.
125
125
  *
126
- * @param input Component render input for the current boundary.
126
+ * @param input Component render input for the current render step.
127
127
  * @param context React-specific render context for stable token generation.
128
128
  * @returns Serialized component HTML with resolved child markup preserved.
129
129
  */
@@ -131,31 +131,31 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
131
131
  /**
132
132
  * Restores raw child HTML that was temporarily replaced by a token during React SSR.
133
133
  *
134
- * Queued boundary resolution may render children through a fragment path before all
134
+ * Queued foreign-subtree resolution may render children through a fragment path before all
135
135
  * nested integration tokens are resolved. When that happens, React must never see
136
136
  * the resolved child HTML as a normal string child or it would escape it. The
137
137
  * runtime context stores the placeholder token and the raw child HTML so the
138
- * fragment render path can reinsert it before foreign boundary tokens are handled.
138
+ * fragment render path can reinsert it before foreign-subtree tokens are handled.
139
139
  */
140
140
  private restoreRuntimeChildHtml;
141
141
  /**
142
- * Renders queued child content through React and then resolves nested boundary tokens.
142
+ * Renders queued child content through React and then resolves nested foreign-subtree tokens.
143
143
  *
144
144
  * This path is only used for children that were deferred while React rendered the
145
- * parent boundary. It first restores any raw child HTML placeholders owned by the
146
- * current runtime context, then asks the shared queued-boundary resolver to swap
145
+ * parent component. It first restores any raw child HTML placeholders owned by the
146
+ * current runtime context, then asks the shared queued foreign-subtree resolver to swap
147
147
  * foreign integration tokens with their resolved HTML.
148
148
  */
149
149
  private renderQueuedChildrenToHtml;
150
150
  /**
151
- * Resolves queued renderer-owned boundary tokens produced during React component rendering.
151
+ * Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
152
152
  *
153
- * React components can enqueue nested boundaries while the parent HTML is being
153
+ * React components can enqueue nested foreign subtrees while the parent HTML is being
154
154
  * rendered. This delegates to the shared renderer-owned queue resolver but keeps
155
155
  * the React-specific child rendering behavior local so raw child HTML and React's
156
156
  * fragment rendering semantics stay coordinated.
157
157
  */
158
- private resolveQueuedBoundaryHtml;
158
+ private resolveQueuedForeignSubtreeHtml;
159
159
  private buildHydrationProps;
160
160
  /**
161
161
  * Builds the extra document props needed when React renders through a non-React HTML shell.
@@ -165,21 +165,21 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
165
165
  */
166
166
  private buildNonReactDocumentProps;
167
167
  /**
168
- * Renders a foreign integration component boundary that participates in React composition.
168
+ * Renders a foreign integration component that participates in React composition.
169
169
  *
170
170
  * Non-React components must resolve to serialized HTML so React can embed them as
171
- * mixed-shell boundaries. Any component-owned dependencies still need to flow
172
- * through the shared dependency resolver before queued boundary tokens are finalized.
171
+ * mixed-shell children. Any component-owned dependencies still need to flow
172
+ * through the shared dependency resolver before queued foreign-subtree tokens are finalized.
173
173
  */
174
- private renderForeignComponentBoundary;
174
+ private renderForeignComponentWithSerializedHtml;
175
175
  /**
176
- * Renders a React-owned component boundary and attaches island hydration metadata when possible.
176
+ * Renders a React-owned component and attaches island hydration metadata when possible.
177
177
  *
178
- * This path keeps React-owned SSR, queued boundary resolution, and optional
178
+ * This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
179
179
  * island hydration wiring together so the public `renderComponent()` method can
180
180
  * read as orchestration rather than implementation detail.
181
181
  */
182
- private renderReactComponentBoundary;
182
+ private renderReactManagedComponent;
183
183
  /**
184
184
  * Renders a React component for component-level orchestration.
185
185
  *
@@ -188,17 +188,17 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
188
188
  * - When an explicit component instance id is provided, a stable
189
189
  * `data-eco-component-id` attribute is attached so island hydration can target it.
190
190
  * - Without an explicit instance id, component renders remain plain SSR output.
191
- * - When resolved child HTML is provided, that boundary is treated as a pure SSR
191
+ * - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
192
192
  * composition step and does not emit hydration assets for the parent wrapper.
193
193
  *
194
194
  * This preserves DOM shape for global CSS/layout selectors while keeping a
195
195
  * deterministic mount target per component instance.
196
196
  */
197
197
  renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult>;
198
- protected createComponentBoundaryRuntime(options: {
199
- boundaryInput: ComponentRenderInput;
198
+ protected createForeignChildRuntime(options: {
199
+ renderInput: ComponentRenderInput;
200
200
  rendererCache: Map<string, IntegrationRenderer<any>>;
201
- }): import("@ecopages/core").ComponentBoundaryRuntime;
201
+ }): import("@ecopages/core").ForeignChildRuntime;
202
202
  /**
203
203
  * Checks if the given file path corresponds to an MDX file based on configured extensions.
204
204
  * @param filePath - The file path to check
@@ -208,7 +208,9 @@ export declare class ReactRenderer extends IntegrationRenderer<ReactNode> {
208
208
  protected usesIntegrationPageImporter(file: string): boolean;
209
209
  protected importIntegrationPageFile(file: string, options?: RouteModuleLoadOptions): Promise<EcoPageFile>;
210
210
  protected normalizeImportedPageFile<TPageModule extends EcoPageFile>(file: string, pageModule: TPageModule): TPageModule;
211
- buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]>;
211
+ buildPageBrowserGraph(pagePath: string): Promise<{
212
+ assets: ProcessedAsset[];
213
+ }>;
212
214
  /**
213
215
  * Renders a full route response for the filesystem page pipeline.
214
216
  *
@@ -178,8 +178,8 @@ class ReactRenderer extends IntegrationRenderer {
178
178
  if (!filePath) {
179
179
  return;
180
180
  }
181
- const hydrationAssets = await this.buildRouteRenderAssets(filePath);
182
- this.appendProcessedDependencies(hydrationAssets);
181
+ const pageBrowserGraph = await this.buildPageBrowserGraph(filePath);
182
+ this.appendProcessedDependencies(pageBrowserGraph.assets);
183
183
  }
184
184
  /**
185
185
  * Renders a non-React layout or HTML template and enforces that mixed shells
@@ -196,22 +196,22 @@ class ReactRenderer extends IntegrationRenderer {
196
196
  throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
197
197
  }
198
198
  /**
199
- * Renders one React component boundary while preserving already-resolved child HTML.
199
+ * Renders one React component while preserving already-resolved child HTML.
200
200
  *
201
- * When nested boundary resolution has already produced child HTML for this
202
- * boundary, the child payload must remain raw SSR output rather than a React
201
+ * When nested foreign-subtree resolution has already produced child HTML for this
202
+ * component, the child payload must remain raw SSR output rather than a React
203
203
  * string child, otherwise React would escape it. This helper renders a unique
204
204
  * token through React and swaps that token back to the resolved HTML
205
205
  * afterward.
206
206
  *
207
- * @param input Component render input for the current boundary.
207
+ * @param input Component render input for the current render step.
208
208
  * @param context React-specific render context for stable token generation.
209
209
  * @returns Serialized component HTML with resolved child markup preserved.
210
210
  */
211
211
  renderComponentHtml(input, context, runtimeContext) {
212
212
  const { react, reactDomServer } = this.getReactRuntimeModules();
213
213
  if (input.children === void 0) {
214
- return this.normalizeBoundaryArtifactHtml(
214
+ return this.normalizeUnresolvedMarkerArtifactHtml(
215
215
  reactDomServer.renderToString(react.createElement(this.asReactComponent(input.component), input.props))
216
216
  );
217
217
  }
@@ -224,16 +224,16 @@ class ReactRenderer extends IntegrationRenderer {
224
224
  const html = reactDomServer.renderToString(
225
225
  react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
226
226
  );
227
- return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
227
+ return this.normalizeUnresolvedMarkerArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
228
228
  }
229
229
  /**
230
230
  * Restores raw child HTML that was temporarily replaced by a token during React SSR.
231
231
  *
232
- * Queued boundary resolution may render children through a fragment path before all
232
+ * Queued foreign-subtree resolution may render children through a fragment path before all
233
233
  * nested integration tokens are resolved. When that happens, React must never see
234
234
  * the resolved child HTML as a normal string child or it would escape it. The
235
235
  * runtime context stores the placeholder token and the raw child HTML so the
236
- * fragment render path can reinsert it before foreign boundary tokens are handled.
236
+ * fragment render path can reinsert it before foreign-subtree tokens are handled.
237
237
  */
238
238
  restoreRuntimeChildHtml(html, runtimeContext) {
239
239
  if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
@@ -242,11 +242,11 @@ class ReactRenderer extends IntegrationRenderer {
242
242
  return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
243
243
  }
244
244
  /**
245
- * Renders queued child content through React and then resolves nested boundary tokens.
245
+ * Renders queued child content through React and then resolves nested foreign-subtree tokens.
246
246
  *
247
247
  * This path is only used for children that were deferred while React rendered the
248
- * parent boundary. It first restores any raw child HTML placeholders owned by the
249
- * current runtime context, then asks the shared queued-boundary resolver to swap
248
+ * parent component. It first restores any raw child HTML placeholders owned by the
249
+ * current runtime context, then asks the shared queued foreign-subtree resolver to swap
250
250
  * foreign integration tokens with their resolved HTML.
251
251
  */
252
252
  async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
@@ -254,26 +254,34 @@ class ReactRenderer extends IntegrationRenderer {
254
254
  return void 0;
255
255
  }
256
256
  const { react, reactDomServer } = this.getReactRuntimeModules();
257
- let html = this.normalizeBoundaryArtifactHtml(
257
+ let html = this.normalizeUnresolvedMarkerArtifactHtml(
258
258
  reactDomServer.renderToString(react.createElement(react.Fragment, null, children))
259
259
  );
260
260
  html = this.restoreRuntimeChildHtml(html, runtimeContext);
261
- html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
261
+ html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(
262
+ html,
263
+ queuedResolutionsByToken,
264
+ resolveToken
265
+ );
262
266
  return html;
263
267
  }
264
268
  /**
265
- * Resolves queued renderer-owned boundary tokens produced during React component rendering.
269
+ * Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
266
270
  *
267
- * React components can enqueue nested boundaries while the parent HTML is being
271
+ * React components can enqueue nested foreign subtrees while the parent HTML is being
268
272
  * rendered. This delegates to the shared renderer-owned queue resolver but keeps
269
273
  * the React-specific child rendering behavior local so raw child HTML and React's
270
274
  * fragment rendering semantics stay coordinated.
271
275
  */
272
- async resolveQueuedBoundaryHtml(html, runtimeContext) {
273
- return this.resolveRendererOwnedQueuedBoundaryHtml({
276
+ async resolveQueuedForeignSubtreeHtml(html, runtimeContext) {
277
+ return this.foreignSubtreeExecutionService.resolveQueuedHtml({
278
+ currentIntegrationName: this.name,
274
279
  html,
275
280
  runtimeContext,
276
281
  queueLabel: "React",
282
+ getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
283
+ applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
284
+ dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
277
285
  renderQueuedChildren: async (children, currentRuntimeContext, queuedResolutionsByToken, resolveToken) => {
278
286
  const renderedHtml = await this.renderQueuedChildrenToHtml(
279
287
  children,
@@ -310,13 +318,13 @@ class ReactRenderer extends IntegrationRenderer {
310
318
  };
311
319
  }
312
320
  /**
313
- * Renders a foreign integration component boundary that participates in React composition.
321
+ * Renders a foreign integration component that participates in React composition.
314
322
  *
315
323
  * Non-React components must resolve to serialized HTML so React can embed them as
316
- * mixed-shell boundaries. Any component-owned dependencies still need to flow
317
- * through the shared dependency resolver before queued boundary tokens are finalized.
324
+ * mixed-shell children. Any component-owned dependencies still need to flow
325
+ * through the shared dependency resolver before queued foreign-subtree tokens are finalized.
318
326
  */
319
- async renderForeignComponentBoundary(input, runtimeContext) {
327
+ async renderForeignComponentWithSerializedHtml(input, runtimeContext) {
320
328
  let props = input.props;
321
329
  if (input.children !== void 0) {
322
330
  props = {
@@ -332,35 +340,35 @@ class ReactRenderer extends IntegrationRenderer {
332
340
  const hasDependencies = Boolean(input.component.config?.dependencies);
333
341
  const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
334
342
  const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
335
- const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
343
+ const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
336
344
  const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
337
345
  ...assets ?? [],
338
- ...queuedBoundaryResolution.assets
346
+ ...queuedForeignSubtreeResolution.assets
339
347
  ]);
340
348
  return {
341
- html: queuedBoundaryResolution.html,
349
+ html: queuedForeignSubtreeResolution.html,
342
350
  canAttachAttributes: true,
343
- rootTag: this.getRootTagName(queuedBoundaryResolution.html),
351
+ rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
344
352
  integrationName: this.name,
345
353
  assets: mergedAssets.length > 0 ? mergedAssets : void 0
346
354
  };
347
355
  }
348
356
  /**
349
- * Renders a React-owned component boundary and attaches island hydration metadata when possible.
357
+ * Renders a React-owned component and attaches island hydration metadata when possible.
350
358
  *
351
- * This path keeps React-owned SSR, queued boundary resolution, and optional
359
+ * This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
352
360
  * island hydration wiring together so the public `renderComponent()` method can
353
361
  * read as orchestration rather than implementation detail.
354
362
  */
355
- async renderReactComponentBoundary(input, runtimeContext) {
363
+ async renderReactManagedComponent(input, runtimeContext) {
356
364
  const componentConfig = input.component.config;
357
365
  const context = {
358
366
  componentInstanceId: input.integrationContext?.componentInstanceId
359
367
  };
360
368
  const hasResolvedChildHtml = input.children !== void 0;
361
369
  let html = this.renderComponentHtml(input, context, runtimeContext);
362
- const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
363
- html = queuedBoundaryResolution.html;
370
+ const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
371
+ html = queuedForeignSubtreeResolution.html;
364
372
  const canAttachAttributes = hasSingleRootElement(html);
365
373
  const rootTag = this.getRootTagName(html);
366
374
  const componentFile = componentConfig?.__eco?.file;
@@ -377,7 +385,7 @@ class ReactRenderer extends IntegrationRenderer {
377
385
  }
378
386
  const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
379
387
  ...assets ?? [],
380
- ...queuedBoundaryResolution.assets
388
+ ...queuedForeignSubtreeResolution.assets
381
389
  ]);
382
390
  return {
383
391
  html,
@@ -396,27 +404,27 @@ class ReactRenderer extends IntegrationRenderer {
396
404
  * - When an explicit component instance id is provided, a stable
397
405
  * `data-eco-component-id` attribute is attached so island hydration can target it.
398
406
  * - Without an explicit instance id, component renders remain plain SSR output.
399
- * - When resolved child HTML is provided, that boundary is treated as a pure SSR
407
+ * - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
400
408
  * composition step and does not emit hydration assets for the parent wrapper.
401
409
  *
402
410
  * This preserves DOM shape for global CSS/layout selectors while keeping a
403
411
  * deterministic mount target per component instance.
404
412
  */
405
413
  async renderComponent(input) {
406
- const runtimeContext = this.getQueuedBoundaryRuntime(input);
414
+ const runtimeContext = this.getQueuedForeignSubtreeResolutionContext(input);
407
415
  if (!this.isReactManagedComponent(input.component)) {
408
- return this.renderForeignComponentBoundary(input, runtimeContext);
416
+ return this.renderForeignComponentWithSerializedHtml(input, runtimeContext);
409
417
  }
410
- return this.renderReactComponentBoundary(input, runtimeContext);
418
+ return this.renderReactManagedComponent(input, runtimeContext);
411
419
  }
412
- createComponentBoundaryRuntime(options) {
413
- return this.createQueuedBoundaryRuntime({
414
- boundaryInput: options.boundaryInput,
420
+ createForeignChildRuntime(options) {
421
+ return this.createQueuedForeignSubtreeExecutionRuntime({
422
+ renderInput: options.renderInput,
415
423
  rendererCache: options.rendererCache,
416
424
  createRuntimeContext: (integrationContext, rendererCache) => ({
417
425
  rendererCache,
418
426
  componentInstanceScope: integrationContext.componentInstanceId,
419
- nextBoundaryId: 0,
427
+ nextForeignSubtreeId: 0,
420
428
  queuedResolutions: [],
421
429
  rawChildrenToken: void 0,
422
430
  rawChildrenHtml: void 0
@@ -450,16 +458,16 @@ class ReactRenderer extends IntegrationRenderer {
450
458
  config
451
459
  };
452
460
  }
453
- async buildRouteRenderAssets(pagePath) {
461
+ async buildPageBrowserGraph(pagePath) {
454
462
  try {
455
463
  const pageModule = await this.importPageFile(pagePath);
456
464
  const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
457
465
  if (!shouldHydrate) {
458
- return [];
466
+ return { assets: [] };
459
467
  }
460
468
  const isMdx = this.pageModuleService.isMdxFile(pagePath);
461
469
  const declaredModules = this.pageModuleService.collectPageDeclaredModules(pageModule);
462
- const processedAssets = await this.hydrationAssetService.buildRouteRenderAssets(
470
+ const processedAssets = await this.hydrationAssetService.buildPageBrowserGraphAssets(
463
471
  pagePath,
464
472
  isMdx,
465
473
  declaredModules
@@ -470,9 +478,9 @@ class ReactRenderer extends IntegrationRenderer {
470
478
  config: pageModule.config,
471
479
  processComponentDependencies: async (components) => await this.processComponentDependencies(components)
472
480
  });
473
- return [...processedAssets, ...mdxConfigAssets];
481
+ return { assets: [...processedAssets, ...mdxConfigAssets] };
474
482
  }
475
- return processedAssets;
483
+ return { assets: processedAssets };
476
484
  } catch (error) {
477
485
  if (error instanceof BundleError) {
478
486
  console.error("[ecopages] Bundle errors:", error.logs);
@@ -560,16 +568,16 @@ class ReactRenderer extends IntegrationRenderer {
560
568
  const metadata = await this.resolveViewMetadata(view, props);
561
569
  await this.prepareViewDependencies(view, Layout);
562
570
  await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
563
- const viewRender = await this.renderComponentBoundary({
571
+ const viewRender = await this.renderComponentWithForeignChildren({
564
572
  component: view,
565
573
  props: normalizedProps
566
574
  });
567
- const layoutRender = Layout ? await this.renderComponentBoundary({
575
+ const layoutRender = Layout ? await this.renderComponentWithForeignChildren({
568
576
  component: Layout,
569
577
  props: {},
570
578
  children: viewRender.html
571
579
  }) : void 0;
572
- const documentRender = await this.renderComponentBoundary({
580
+ const documentRender = await this.renderComponentWithForeignChildren({
573
581
  component: HtmlTemplate,
574
582
  props: {
575
583
  metadata,
@@ -1,5 +1,4 @@
1
- import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
2
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import { IntegrationPlugin, type EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
3
2
  import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
4
3
  import type React from 'react';
5
4
  import { ReactRenderer } from './react-renderer.js';
@@ -13,7 +12,7 @@ export declare const PLUGIN_NAME = "react";
13
12
  * The React plugin class
14
13
  * This plugin provides support for React components in Ecopages
15
14
  */
16
- export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
15
+ export declare class ReactPlugin extends IntegrationPlugin<React.ReactNode> {
17
16
  renderer: typeof ReactRenderer;
18
17
  private readonly routerAdapter;
19
18
  private readonly mdxEnabled;
@@ -66,7 +65,6 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
66
65
  * @returns ReactHmrStrategy instance for handling React component updates
67
66
  */
68
67
  getHmrStrategy(): HmrStrategy | undefined;
69
- getRuntimeSpecifierMap(): Record<string, string>;
70
68
  }
71
69
  /**
72
70
  * Factory function to create a React plugin instance
@@ -1,4 +1,6 @@
1
- import { IntegrationPlugin } from "@ecopages/core/plugins/integration-plugin";
1
+ import {
2
+ IntegrationPlugin
3
+ } from "@ecopages/core/plugins/integration-plugin";
2
4
  import { Logger } from "@ecopages/logger";
3
5
  import { REACT_PLUGIN_NAME } from "./react.constants.js";
4
6
  import { ReactRenderer } from "./react-renderer.js";
@@ -174,17 +176,15 @@ class ReactPlugin extends IntegrationPlugin {
174
176
  return void 0;
175
177
  }
176
178
  const context = this.hmrManager.getDefaultContext();
177
- return new ReactHmrStrategy(
179
+ return new ReactHmrStrategy({
178
180
  context,
179
- this.hmrPageMetadataCache,
180
- this.mdxCompilerOptions,
181
- this.extensions,
182
- this.appConfig.templatesExt,
183
- this.explicitGraphEnabled
184
- );
185
- }
186
- getRuntimeSpecifierMap() {
187
- return this.runtimeBundleService.getSpecifierMap();
181
+ pageMetadataCache: this.hmrPageMetadataCache,
182
+ runtimeAliasMap: new Map(Object.entries(this.runtimeBundleService.getRuntimeAliasMap("development"))),
183
+ mdxCompilerOptions: this.mdxCompilerOptions,
184
+ ownedTemplateExtensions: this.extensions,
185
+ allTemplateExtensions: this.appConfig.templatesExt,
186
+ explicitGraphEnabled: this.explicitGraphEnabled
187
+ });
188
188
  }
189
189
  }
190
190
  function reactPlugin(options) {
@@ -12,11 +12,10 @@
12
12
  * const myRouter: ReactRouterAdapter = {
13
13
  * name: 'my-router',
14
14
  * bundle: {
15
- * importPath: '@my/router/browser.ts',
15
+ * importPath: '@my/router/browser',
16
16
  * outputName: 'my-router',
17
17
  * externals: ['react', 'react-dom'],
18
18
  * },
19
- * importMapKey: '@my/router',
20
19
  * components: {
21
20
  * router: 'MyRouter',
22
21
  * pageContent: 'PageOutlet',
@@ -36,39 +35,33 @@ export interface ReactRouterAdapter {
36
35
  bundle: {
37
36
  /**
38
37
  * Node module import path for the browser-compatible entry.
39
- * @example '@ecopages/react-router/browser.ts'
38
+ * @example '@ecopages/react-router/browser'
40
39
  */
41
40
  importPath: string;
42
41
  /**
43
42
  * Output filename (without extension).
44
- * @example 'react-router-esm'
43
+ * @example 'my-router'
45
44
  */
46
45
  outputName: string;
47
46
  /**
48
47
  * Packages to externalize when bundling.
49
- * These should be available through the runtime bare-specifier map.
50
- * @example ['react', 'react-dom', 'react/jsx-runtime']
48
+ * These should stay as bare runtime dependencies for the router bundle.
49
+ * @example ['react', 'react-dom']
51
50
  */
52
51
  externals: string[];
53
52
  };
54
- /**
55
- * Bare specifier for the runtime mapping entry.
56
- * This is what the hydration script will import from.
57
- * @example '@ecopages/react-router'
58
- */
59
- importMapKey: string;
60
53
  /**
61
54
  * Component names to import from the router package.
62
55
  */
63
56
  components: {
64
57
  /**
65
58
  * The router component that wraps the layout.
66
- * @example 'EcoRouter'
59
+ * @example 'MyRouter'
67
60
  */
68
61
  router: string;
69
62
  /**
70
63
  * The component that renders the current page content.
71
- * @example 'PageContent'
64
+ * @example 'PageOutlet'
72
65
  */
73
66
  pageContent: string;
74
67
  };
@@ -53,5 +53,5 @@ export declare class ReactBundleService {
53
53
  * Creates the esbuild plugin that rewrites bare React specifiers
54
54
  * to their runtime asset URLs.
55
55
  */
56
- createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("@ecopages/core/build/build-types").EcoBuildPlugin | null;
56
+ createRuntimeAliasPlugin(runtimeAliasMap: Record<string, string>): import("@ecopages/core/build/build-types").EcoBuildPlugin | null;
57
57
  }
@@ -1,9 +1,9 @@
1
1
  import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
2
2
  import {
3
- buildReactRuntimeSpecifierMap,
3
+ buildReactRuntimeAliasMap,
4
4
  getReactClientGraphAllowSpecifiers,
5
5
  getReactRuntimeExternalSpecifiers
6
- } from "../utils/react-runtime-specifier-map.js";
6
+ } from "../utils/react-runtime-alias-map.js";
7
7
  import { createUseSyncExternalStoreShimPlugin } from "../utils/use-sync-external-store-shim-plugin.js";
8
8
  import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
9
9
  import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
@@ -33,6 +33,7 @@ class ReactBundleService {
33
33
  * @returns Bundle options object for the build adapter
34
34
  */
35
35
  async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
36
+ const runtimeImports = this.getRuntimeImports();
36
37
  const options = {
37
38
  mainFields: ["module", "browser", "main"],
38
39
  naming: `${componentName}.[ext]`,
@@ -43,7 +44,10 @@ class ReactBundleService {
43
44
  }
44
45
  };
45
46
  if (!bundleOptions.includeRuntime) {
46
- options.external = getReactRuntimeExternalSpecifiers();
47
+ options.external = [
48
+ ...getReactRuntimeExternalSpecifiers(),
49
+ ...Object.values(runtimeImports).filter((specifier) => Boolean(specifier))
50
+ ];
47
51
  }
48
52
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
49
53
  absWorkingDir: this.config.rootDir,
@@ -59,11 +63,7 @@ class ReactBundleService {
59
63
  name: "react-renderer-use-sync-external-store-shim",
60
64
  namespace: "ecopages-react-renderer-shim"
61
65
  });
62
- const runtimePlugins = bundleOptions.includeRuntime ? [] : [
63
- this.createRuntimeAliasPlugin(
64
- buildReactRuntimeSpecifierMap(this.getRuntimeImports(), this.config.routerAdapter)
65
- )
66
- ];
66
+ const runtimePlugins = bundleOptions.includeRuntime ? [] : [this.createRuntimeAliasPlugin(buildReactRuntimeAliasMap(runtimeImports))];
67
67
  if (isMdx && this.config.mdxCompilerOptions) {
68
68
  const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
69
69
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
@@ -88,8 +88,8 @@ class ReactBundleService {
88
88
  * Creates the esbuild plugin that rewrites bare React specifiers
89
89
  * to their runtime asset URLs.
90
90
  */
91
- createRuntimeAliasPlugin(runtimeSpecifierMap) {
92
- return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: "react-runtime-import-alias" });
91
+ createRuntimeAliasPlugin(runtimeAliasMap) {
92
+ return createRuntimeSpecifierAliasPlugin(runtimeAliasMap, { name: "react-runtime-import-alias" });
93
93
  }
94
94
  }
95
95
  export {
@@ -51,7 +51,7 @@ export declare class ReactHydrationAssetService {
51
51
  * @param isMdx - Whether the source file is an MDX file
52
52
  * @returns One page-owned asset definition for processing
53
53
  */
54
- createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, isMdx: boolean): AssetDefinition[];
54
+ createPageDependencies(pagePath: string, componentName: string, importPath: string, pageModuleUrlExpression: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, useBrowserRuntimeImports: boolean, isMdx: boolean): AssetDefinition[];
55
55
  /**
56
56
  * Builds client-side assets for a React component island.
57
57
  *
@@ -63,12 +63,12 @@ export declare class ReactHydrationAssetService {
63
63
  */
64
64
  buildComponentRenderAssets(componentFile: string, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
65
65
  /**
66
- * Builds all client-side route assets for a page.
66
+ * Builds the Page Browser Graph assets for a React page.
67
67
  *
68
68
  * @param pagePath - Absolute file path of the page
69
69
  * @param isMdx - Whether the page is an MDX file
70
70
  * @param declaredModules - Explicitly declared browser module specifiers
71
71
  * @returns Processed assets for the route
72
72
  */
73
- buildRouteRenderAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
73
+ buildPageBrowserGraphAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
74
74
  }
@@ -46,16 +46,17 @@ class ReactHydrationAssetService {
46
46
  * @param isMdx - Whether the source file is an MDX file
47
47
  * @returns One page-owned asset definition for processing
48
48
  */
49
- createPageDependencies(pagePath, componentName, importPath, bundleOptions, isDevelopment, isMdx) {
49
+ createPageDependencies(pagePath, componentName, importPath, pageModuleUrlExpression, bundleOptions, isDevelopment, useBrowserRuntimeImports, isMdx) {
50
50
  const runtimeImports = this.config.bundleService.getRuntimeImports();
51
51
  return [
52
52
  AssetFactory.createContentScript({
53
53
  position: "head",
54
54
  content: createHydrationScript({
55
55
  importPath: isDevelopment ? importPath : pagePath,
56
- reactImportPath: isDevelopment ? runtimeImports.react : "react",
57
- reactDomClientImportPath: isDevelopment ? runtimeImports.reactDomClient : "react-dom/client",
58
- routerImportPath: isDevelopment ? runtimeImports.router : this.config.routerAdapter?.importMapKey,
56
+ pageModuleUrlExpression,
57
+ reactImportPath: useBrowserRuntimeImports ? runtimeImports.react : "react",
58
+ reactDomClientImportPath: useBrowserRuntimeImports ? runtimeImports.reactDomClient : "react-dom/client",
59
+ routerImportPath: useBrowserRuntimeImports ? runtimeImports.router : this.config.routerAdapter?.bundle.importPath,
59
60
  isDevelopment,
60
61
  isMdx,
61
62
  router: this.config.routerAdapter,
@@ -145,32 +146,38 @@ class ReactHydrationAssetService {
145
146
  return this.config.assetProcessingService.processDependencies(dependencies, componentName);
146
147
  }
147
148
  /**
148
- * Builds all client-side route assets for a page.
149
+ * Builds the Page Browser Graph assets for a React page.
149
150
  *
150
151
  * @param pagePath - Absolute file path of the page
151
152
  * @param isMdx - Whether the page is an MDX file
152
153
  * @param declaredModules - Explicitly declared browser module specifiers
153
154
  * @returns Processed assets for the route
154
155
  */
155
- async buildRouteRenderAssets(pagePath, isMdx, declaredModules) {
156
+ async buildPageBrowserGraphAssets(pagePath, isMdx, declaredModules) {
156
157
  const componentName = `ecopages-react-${rapidhash(pagePath)}`;
157
158
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
158
159
  const isDevelopment = hmrManager?.isEnabled() ?? false;
160
+ const isHostedDevelopment = !isDevelopment && process.env.NODE_ENV !== "production";
161
+ const useBrowserRuntimeImports = isDevelopment || isHostedDevelopment;
159
162
  if (isDevelopment) {
160
163
  this.config.hmrPageMetadataCache?.setDeclaredModules(pagePath, declaredModules);
161
164
  }
162
165
  const importPath = await this.resolveAssetImportPath(pagePath, componentName);
166
+ const pageModuleUrlExpression = "import.meta.url";
163
167
  const bundleOptions = await this.config.bundleService.createBundleOptions(
164
168
  componentName,
165
169
  isMdx,
166
- declaredModules
170
+ declaredModules,
171
+ { includeRuntime: !useBrowserRuntimeImports }
167
172
  );
168
173
  const dependencies = this.createPageDependencies(
169
174
  pagePath,
170
175
  componentName,
171
176
  importPath,
177
+ pageModuleUrlExpression,
172
178
  bundleOptions,
173
179
  isDevelopment,
180
+ useBrowserRuntimeImports,
174
181
  isMdx
175
182
  );
176
183
  if (!this.config.assetProcessingService) {
@@ -1,4 +1,4 @@
1
- import { LocalsAccessError } from "@ecopages/core/errors/locals-access-error";
1
+ import { LocalsAccessError } from "@ecopages/core/errors";
2
2
  class ReactPagePayloadService {
3
3
  /**
4
4
  * Creates the canonical page-props payload used by router hydration.
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module
8
8
  */
9
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
9
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
10
10
  import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
11
11
  import type { ReactRouterAdapter } from '../router-adapter.js';
12
12
  export type ReactRuntimeImports = {
@@ -33,7 +33,7 @@ export declare class ReactRuntimeBundleService {
33
33
  private getReactDomVendorFileName;
34
34
  private getRouterVendorFileName;
35
35
  getRuntimeImports(mode?: RuntimeMode): ReactRuntimeImports;
36
- getSpecifierMap(mode?: RuntimeMode): Record<string, string>;
36
+ getRuntimeAliasMap(mode?: RuntimeMode): Record<string, string>;
37
37
  getDependencies(): AssetDefinition[];
38
38
  createRuntimeAliasPlugin(mode?: RuntimeMode): EcoBuildPlugin;
39
39
  }
@@ -5,7 +5,7 @@ import {
5
5
  createBrowserRuntimeScriptAsset
6
6
  } from "@ecopages/core/services/asset-processing-service";
7
7
  import { createReactDomRuntimeInteropPlugin } from "../utils/react-dom-runtime-interop-plugin.js";
8
- import { buildReactRuntimeSpecifierMap } from "../utils/react-runtime-specifier-map.js";
8
+ import { buildReactRuntimeAliasMap } from "../utils/react-runtime-alias-map.js";
9
9
  class ReactRuntimeBundleService {
10
10
  config;
11
11
  constructor(config) {
@@ -54,8 +54,8 @@ class ReactRuntimeBundleService {
54
54
  }
55
55
  return runtimeImports;
56
56
  }
57
- getSpecifierMap(mode = this.getCurrentRuntimeMode()) {
58
- return buildReactRuntimeSpecifierMap(this.getRuntimeImports(mode), this.config.routerAdapter);
57
+ getRuntimeAliasMap(mode = this.getCurrentRuntimeMode()) {
58
+ return buildReactRuntimeAliasMap(this.getRuntimeImports(mode));
59
59
  }
60
60
  getDependencies() {
61
61
  const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
@@ -71,7 +71,7 @@ class ReactRuntimeBundleService {
71
71
  (plugin) => plugin !== null
72
72
  );
73
73
  const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
74
- const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap(mode)));
74
+ const mappedSpecifiers = new Set(Object.keys(this.getRuntimeAliasMap(mode)));
75
75
  dependencies.push(
76
76
  createBrowserRuntimeModuleAsset({
77
77
  modules: [
@@ -120,7 +120,7 @@ class ReactRuntimeBundleService {
120
120
  return dependencies;
121
121
  }
122
122
  createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
123
- return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
123
+ return createRuntimeSpecifierAliasPlugin(this.getRuntimeAliasMap(mode), {
124
124
  name: `react-plugin-runtime-alias-${mode}`
125
125
  });
126
126
  }
@@ -14,7 +14,7 @@
14
14
  * Additionally, this plugin provides a build-time transform that statically resolves and
15
15
  * inlines `fs.readFileSync(path.resolve(...))` calls to prevent server/client data mismatches.
16
16
  */
17
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
17
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
18
18
  /**
19
19
  * Configuration options for the Client Graph Boundary esbuild plugin.
20
20
  *
@@ -10,6 +10,8 @@ import type { ReactRouterAdapter } from '../router-adapter.js';
10
10
  export type HydrationScriptOptions = {
11
11
  /** The module path imported by the page entry module. */
12
12
  importPath: string;
13
+ /** Browser expression that resolves to the page module URL the router should import. */
14
+ pageModuleUrlExpression?: string;
13
15
  /** Stable id of the page entry script tag in the document. */
14
16
  scriptId: string;
15
17
  /** Direct import path for React runtime module */
@@ -60,6 +60,7 @@ function getProdReuseExistingRouterRootScript() {
60
60
  }
61
61
  function createDevScriptWithRouter(options) {
62
62
  const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
63
+ const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
63
64
  const { components, getRouterProps } = router;
64
65
  if (!routerImportPath) {
65
66
  throw new Error("routerImportPath is required when router adapter is configured");
@@ -69,7 +70,7 @@ import { hydrateRoot } from "${reactDomClientImportPath}";
69
70
  import { createElement } from "${reactImportPath}";
70
71
  import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
71
72
  ${getImportStatement(importPath, isMdx)}
72
- const pageModuleUrl = import.meta.url;
73
+ const pageModuleUrl = ${pageModuleUrlExpression};
73
74
  export default Page;
74
75
  export const config = Page.config;
75
76
  const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
@@ -157,11 +158,12 @@ if (document.readyState === "loading") {
157
158
  }
158
159
  function createDevScriptWithoutRouter(options) {
159
160
  const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
161
+ const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
160
162
  return `
161
163
  import { hydrateRoot } from "${reactDomClientImportPath}";
162
164
  import { createElement } from "${reactImportPath}";
163
165
  ${getImportStatement(importPath, isMdx)}
164
- const pageModuleUrl = import.meta.url;
166
+ const pageModuleUrl = ${pageModuleUrlExpression};
165
167
  export default Page;
166
168
  export const config = Page.config;
167
169
  const isActivePageEntry = Boolean(document.querySelector('script[data-eco-script-id="${scriptId}"]'));
@@ -229,21 +231,23 @@ if (document.readyState === "loading") {
229
231
  }
230
232
  function createProdScriptWithRouter(options) {
231
233
  const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath, scriptId } = options;
234
+ const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
232
235
  const { components, getRouterProps } = router;
233
236
  if (!routerImportPath) {
234
237
  throw new Error("routerImportPath is required when router adapter is configured");
235
238
  }
236
239
  if (isMdx) {
237
- return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=import.meta.url;export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
240
+ return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
238
241
  }
239
- return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const u=import.meta.url;export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
242
+ return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}${getProdRouterBootstrapRegistrationScript()}${getProdReuseExistingRouterRootScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>ce(R,${getRouterProps("C", "p")},ce(PC));const m=()=>{if(sr()){root=window.__ECO_PAGES__.react.pageRoot;return}if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
240
243
  }
241
244
  function createProdScriptWithoutRouter(options) {
242
245
  const { importPath, isMdx, reactImportPath, reactDomClientImportPath, scriptId } = options;
246
+ const pageModuleUrlExpression = options.pageModuleUrlExpression ?? "import.meta.url";
243
247
  if (isMdx) {
244
- return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=import.meta.url;export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
248
+ return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
245
249
  }
246
- return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u=import.meta.url;export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
250
+ return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const u=${pageModuleUrlExpression};export default P;export const config=P.config;const a=!!document.querySelector('script[data-eco-script-id="${scriptId}"]');if(a){window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.react=window.__ECO_PAGES__.react||{};window.__ECO_PAGES__.react.pageRoot=window.__ECO_PAGES__.react.pageRoot||null;let root=window.__ECO_PAGES__.react.pageRoot;${getProdPageRootCleanupScript()}const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGES__.page={module:u,props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);const lp=p?.locals?{locals:p.locals}:null;return L?ce(L,lp,pe):pe};const m=()=>{if(window.__ECO_PAGES__.react?.pageRoot){root=window.__ECO_PAGES__.react.pageRoot;root.render(ct(P,pr));return}root=hr(document.body,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});window.__ECO_PAGES__.react.pageRoot=root};document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()}`;
247
251
  }
248
252
  function createHydrationScript(options) {
249
253
  const { isDevelopment, router } = options;
@@ -7,7 +7,6 @@ const routerAdapter = {
7
7
  outputName: "router",
8
8
  externals: []
9
9
  },
10
- importMapKey: "@ecopages/react-router",
11
10
  components: {
12
11
  router: "EcoRouter",
13
12
  pageContent: "PageContent"
@@ -1,4 +1,4 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
2
  export declare function createReactDomRuntimeInteropPlugin(options?: {
3
3
  name?: string;
4
4
  reactSpecifier?: string;
@@ -1,3 +1,3 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
2
  import { type CompileOptions } from '@mdx-js/mdx';
3
3
  export declare function createReactMdxLoaderPlugin(compilerOptions?: CompileOptions): EcoBuildPlugin;
@@ -1,6 +1,6 @@
1
1
  import type { ReactRouterAdapter } from '../router-adapter.js';
2
2
  import type { ReactRuntimeImports } from '../services/react-runtime-bundle.service.js';
3
3
  export declare const REACT_RUNTIME_SPECIFIERS: readonly ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"];
4
- export declare function buildReactRuntimeSpecifierMap(runtimeImports: ReactRuntimeImports, routerAdapter?: ReactRouterAdapter): Record<string, string>;
4
+ export declare function buildReactRuntimeAliasMap(runtimeImports: ReactRuntimeImports): Record<string, string>;
5
5
  export declare function getReactRuntimeExternalSpecifiers(): string[];
6
6
  export declare function getReactClientGraphAllowSpecifiers(runtimeSpecifiers: Iterable<string>, routerAdapter?: ReactRouterAdapter): string[];
@@ -5,18 +5,14 @@ const REACT_RUNTIME_SPECIFIERS = [
5
5
  "react/jsx-dev-runtime",
6
6
  "react-dom/client"
7
7
  ];
8
- function buildReactRuntimeSpecifierMap(runtimeImports, routerAdapter) {
9
- const map = {
8
+ function buildReactRuntimeAliasMap(runtimeImports) {
9
+ return {
10
10
  react: runtimeImports.react,
11
11
  "react/jsx-runtime": runtimeImports.reactJsxRuntime,
12
12
  "react/jsx-dev-runtime": runtimeImports.reactJsxDevRuntime,
13
13
  "react-dom": runtimeImports.reactDom,
14
14
  "react-dom/client": runtimeImports.reactDomClient
15
15
  };
16
- if (routerAdapter && runtimeImports.router) {
17
- map[routerAdapter.importMapKey] = runtimeImports.router;
18
- }
19
- return map;
20
16
  }
21
17
  function getReactRuntimeExternalSpecifiers() {
22
18
  return [...REACT_RUNTIME_SPECIFIERS];
@@ -25,13 +21,13 @@ function getReactClientGraphAllowSpecifiers(runtimeSpecifiers, routerAdapter) {
25
21
  return [
26
22
  "@ecopages/core",
27
23
  ...REACT_RUNTIME_SPECIFIERS,
28
- ...routerAdapter ? [routerAdapter.importMapKey] : [],
24
+ ...routerAdapter ? [routerAdapter.bundle.importPath] : [],
29
25
  ...Array.from(runtimeSpecifiers)
30
26
  ];
31
27
  }
32
28
  export {
33
29
  REACT_RUNTIME_SPECIFIERS,
34
- buildReactRuntimeSpecifierMap,
30
+ buildReactRuntimeAliasMap,
35
31
  getReactClientGraphAllowSpecifiers,
36
32
  getReactRuntimeExternalSpecifiers
37
33
  };
@@ -1,4 +1,4 @@
1
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
1
+ import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
2
2
  export declare function createUseSyncExternalStoreShimPlugin(options?: {
3
3
  name?: string;
4
4
  namespace?: string;