@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +30 -13
  2. package/package.json +23 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +102 -18
  6. package/src/react-hmr-strategy.js +427 -50
  7. package/src/react-renderer.d.ts +100 -92
  8. package/src/react-renderer.js +356 -340
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +25 -107
  12. package/src/react.plugin.js +109 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
  17. package/src/runtime/use-sync-external-store-with-selector.js +56 -0
  18. package/src/services/pages-index.d.ts +64 -0
  19. package/src/services/pages-index.js +73 -0
  20. package/src/services/react-bundle.service.d.ts +24 -9
  21. package/src/services/react-bundle.service.js +35 -24
  22. package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
  23. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  24. package/src/services/react-hydration-asset.service.d.ts +28 -19
  25. package/src/services/react-hydration-asset.service.js +83 -64
  26. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  27. package/src/services/react-mdx-config-dependency.service.js +122 -0
  28. package/src/services/react-page-module.service.d.ts +8 -3
  29. package/src/services/react-page-module.service.js +33 -26
  30. package/src/services/react-page-payload.service.d.ts +46 -0
  31. package/src/services/react-page-payload.service.js +67 -0
  32. package/src/services/react-runtime-bundle.service.d.ts +9 -2
  33. package/src/services/react-runtime-bundle.service.js +77 -16
  34. package/src/utils/client-graph-boundary-cache.d.ts +108 -0
  35. package/src/utils/client-graph-boundary-cache.js +116 -0
  36. package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
  37. package/src/utils/client-graph-boundary-plugin.js +63 -5
  38. package/src/utils/component-config-traversal.d.ts +36 -0
  39. package/src/utils/component-config-traversal.js +54 -0
  40. package/src/utils/declared-modules.d.ts +1 -1
  41. package/src/utils/declared-modules.js +7 -16
  42. package/src/utils/dynamic.test.browser.d.ts +1 -0
  43. package/src/utils/dynamic.test.browser.js +33 -0
  44. package/src/utils/hydration-scripts.d.ts +9 -5
  45. package/src/utils/hydration-scripts.js +119 -34
  46. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  47. package/src/utils/hydration-scripts.test.browser.js +198 -0
  48. package/src/utils/react-dom-runtime-interop-plugin.d.ts +1 -1
  49. package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
  50. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  51. package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
  52. package/src/utils/react-runtime-alias-map.js +90 -0
  53. package/CHANGELOG.md +0 -27
  54. package/src/react-hmr-strategy.ts +0 -386
  55. package/src/react-renderer.ts +0 -803
  56. package/src/react.plugin.ts +0 -276
  57. package/src/router-adapter.ts +0 -95
  58. package/src/services/react-bundle.service.ts +0 -108
  59. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  60. package/src/services/react-hydration-asset.service.ts +0 -263
  61. package/src/services/react-page-module.service.ts +0 -224
  62. package/src/services/react-runtime-bundle.service.ts +0 -172
  63. package/src/utils/client-graph-boundary-plugin.ts +0 -831
  64. package/src/utils/client-only.ts +0 -27
  65. package/src/utils/declared-modules.ts +0 -99
  66. package/src/utils/dynamic.ts +0 -27
  67. package/src/utils/hmr-scripts.ts +0 -47
  68. package/src/utils/html-boundary.ts +0 -66
  69. package/src/utils/hydration-scripts.ts +0 -459
  70. package/src/utils/reachability-analyzer.ts +0 -593
  71. package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
  72. package/src/utils/react-mdx-loader-plugin.ts +0 -63
  73. package/src/utils/react-runtime-specifier-map.js +0 -37
  74. package/src/utils/react-runtime-specifier-map.ts +0 -45
  75. package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
  76. package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
  77. package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
@@ -0,0 +1,73 @@
1
+ import path from "node:path";
2
+ import { fileSystem } from "@ecopages/file-system";
3
+ const DEFAULT_EXTENSIONS = [".tsx", ".kita.tsx", ".lit.tsx", ".eco.tsx", ".mdx", ".react.tsx"];
4
+ class PagesIndex {
5
+ pagesDir;
6
+ extensions;
7
+ isPageEntrypoint;
8
+ pages = /* @__PURE__ */ new Set();
9
+ lastRefreshAt = 0;
10
+ constructor(options) {
11
+ this.pagesDir = options.pagesDir;
12
+ this.extensions = options.extensions ?? DEFAULT_EXTENSIONS;
13
+ this.isPageEntrypoint = options.isPageEntrypoint ?? (() => true);
14
+ }
15
+ /**
16
+ * Rescan the pages directory and rebuild the index.
17
+ *
18
+ * Cheap to call multiple times in sequence (just a glob). The
19
+ * real value of `PagesIndex` is the `add` / `remove` path that
20
+ * callers can wire to the file watcher; for now this is the
21
+ * fallback used on layout changes.
22
+ */
23
+ async refresh() {
24
+ const files = await fileSystem.glob(
25
+ this.extensions.map((ext) => `**/*${ext}`),
26
+ {
27
+ cwd: this.pagesDir
28
+ }
29
+ );
30
+ const next = /* @__PURE__ */ new Set();
31
+ for (const file of files) {
32
+ const absolutePath = path.join(this.pagesDir, file);
33
+ if (!this.isPageEntrypoint(absolutePath)) continue;
34
+ if (file.includes(".ecopages-node.")) continue;
35
+ next.add(absolutePath);
36
+ }
37
+ this.pages.clear();
38
+ for (const p of next) this.pages.add(p);
39
+ this.lastRefreshAt = Date.now();
40
+ }
41
+ /**
42
+ * Add a single entrypoint. Idempotent.
43
+ */
44
+ add(absolutePath) {
45
+ if (!this.isPageEntrypoint(absolutePath)) return;
46
+ this.pages.add(absolutePath);
47
+ }
48
+ /**
49
+ * Remove a single entrypoint. Idempotent.
50
+ */
51
+ remove(absolutePath) {
52
+ this.pages.delete(absolutePath);
53
+ }
54
+ /** True if the index contains `absolutePath`. */
55
+ has(absolutePath) {
56
+ return this.pages.has(absolutePath);
57
+ }
58
+ /** Snapshot of the current set, sorted by absolute path. */
59
+ list() {
60
+ return Array.from(this.pages).sort();
61
+ }
62
+ /** Number of indexed entrypoints. */
63
+ get size() {
64
+ return this.pages.size;
65
+ }
66
+ /** Last refresh timestamp (ms since epoch). */
67
+ get refreshedAt() {
68
+ return this.lastRefreshAt;
69
+ }
70
+ }
71
+ export {
72
+ PagesIndex
73
+ };
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Bundle configuration service for React integration.
3
3
  *
4
- * Encapsulates all esbuild plugin creation and bundle options
4
+ * Encapsulates all build plugin creation and bundle options
5
5
  * for client-side React component builds.
6
6
  *
7
7
  * @module
@@ -16,9 +16,25 @@ export interface ReactBundleServiceConfig {
16
16
  rootDir: string;
17
17
  routerAdapter?: ReactRouterAdapter;
18
18
  mdxCompilerOptions?: CompileOptions;
19
+ nonReactExtensions?: string[];
20
+ jsxImportSource?: string;
19
21
  }
20
22
  /**
21
- * Manages esbuild bundle configuration and plugin creation for React page/component builds.
23
+ * Optional flags that adjust how a React client entry is bundled.
24
+ */
25
+ export interface ReactClientBundleOptions {
26
+ /**
27
+ * When `true`, bundle React runtime dependencies into the emitted entry instead of
28
+ * rewriting them to external runtime specifiers.
29
+ */
30
+ includeRuntime?: boolean;
31
+ /**
32
+ * When set, overrides the build adapter chunk splitting mode for this entry.
33
+ */
34
+ splitting?: boolean;
35
+ }
36
+ /**
37
+ * Manages bundle configuration and plugin creation for React page/component builds.
22
38
  */
23
39
  export declare class ReactBundleService {
24
40
  private readonly runtimeBundleService;
@@ -29,17 +45,16 @@ export declare class ReactBundleService {
29
45
  */
30
46
  getRuntimeImports(): ReactRuntimeImports;
31
47
  /**
32
- * Creates esbuild bundle options for a page or component entry.
48
+ * Creates bundle options for a page or component entry.
49
+ *
50
+ * @remarks
51
+ * React derives runtime specifier mappings from the core browser runtime manifest
52
+ * so ESM imports resolve to concrete runtime asset URLs during module loading.
33
53
  *
34
54
  * @param componentName - Generated unique component name for output naming
35
55
  * @param isMdx - Whether the source file is an MDX file
36
56
  * @param declaredModules - Explicitly declared browser module specifiers
37
57
  * @returns Bundle options object for the build adapter
38
58
  */
39
- createBundleOptions(componentName: string, isMdx: boolean, declaredModules: string[]): Promise<Record<string, unknown>>;
40
- /**
41
- * Creates the esbuild plugin that rewrites bare React specifiers
42
- * to their runtime asset URLs.
43
- */
44
- createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("packages/core/src/build/build-types.ts").EcoBuildPlugin | null;
59
+ createBundleOptions(componentName: string, isMdx: boolean, declaredModules: string[], bundleOptions?: ReactClientBundleOptions): Promise<Record<string, unknown>>;
45
60
  }
@@ -1,18 +1,19 @@
1
1
  import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
2
2
  import {
3
- buildReactRuntimeSpecifierMap,
4
3
  getReactClientGraphAllowSpecifiers,
5
4
  getReactRuntimeExternalSpecifiers
6
- } from "../utils/react-runtime-specifier-map.js";
7
- import { createUseSyncExternalStoreShimPlugin } from "../utils/use-sync-external-store-shim-plugin.js";
8
- import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
5
+ } from "../utils/react-runtime-alias-map.js";
6
+ import { createBrowserRuntimePlugin } from "@ecopages/core/build/browser-runtime-plugin";
7
+ import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
9
8
  import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
9
+ import { createReactMdxLoaderPlugin } from "../utils/react-mdx-loader-plugin.js";
10
10
  class ReactBundleService {
11
11
  runtimeBundleService;
12
12
  config;
13
13
  constructor(config) {
14
14
  this.config = config;
15
15
  this.runtimeBundleService = new ReactRuntimeBundleService({
16
+ rootDir: config.rootDir,
16
17
  routerAdapter: config.routerAdapter
17
18
  });
18
19
  }
@@ -23,52 +24,62 @@ class ReactBundleService {
23
24
  return this.runtimeBundleService.getRuntimeImports();
24
25
  }
25
26
  /**
26
- * Creates esbuild bundle options for a page or component entry.
27
+ * Creates bundle options for a page or component entry.
28
+ *
29
+ * @remarks
30
+ * React derives runtime specifier mappings from the core browser runtime manifest
31
+ * so ESM imports resolve to concrete runtime asset URLs during module loading.
27
32
  *
28
33
  * @param componentName - Generated unique component name for output naming
29
34
  * @param isMdx - Whether the source file is an MDX file
30
35
  * @param declaredModules - Explicitly declared browser module specifiers
31
36
  * @returns Bundle options object for the build adapter
32
37
  */
33
- async createBundleOptions(componentName, isMdx, declaredModules) {
38
+ async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
34
39
  const runtimeImports = this.getRuntimeImports();
35
- const runtimeSpecifierMap = buildReactRuntimeSpecifierMap(runtimeImports, this.config.routerAdapter);
36
40
  const options = {
37
- external: getReactRuntimeExternalSpecifiers(),
38
41
  mainFields: ["module", "browser", "main"],
39
42
  naming: `${componentName}.[ext]`,
40
43
  ...import.meta.env?.NODE_ENV === "production" && {
41
44
  minify: true,
42
- splitting: false,
43
45
  treeshaking: true
44
- }
46
+ },
47
+ ...bundleOptions.splitting === void 0 ? {} : { splitting: bundleOptions.splitting }
45
48
  };
49
+ if (!bundleOptions.includeRuntime) {
50
+ const reactRuntimeSpecifiers = new Set(getReactRuntimeExternalSpecifiers());
51
+ options.external = [
52
+ ...Object.values(runtimeImports).filter(
53
+ (specifier) => Boolean(specifier) && !reactRuntimeSpecifiers.has(
54
+ specifier
55
+ )
56
+ )
57
+ ];
58
+ }
46
59
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
47
60
  absWorkingDir: this.config.rootDir,
48
61
  declaredModules,
49
62
  alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
50
63
  });
51
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
52
- const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
53
- name: "react-renderer-use-sync-external-store-shim",
54
- namespace: "ecopages-react-renderer-shim"
64
+ const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
65
+ name: "react-renderer-foreign-jsx-override",
66
+ hostJsxImportSource: this.config.jsxImportSource ?? "react",
67
+ foreignExtensions: this.config.nonReactExtensions ?? []
68
+ });
69
+ const runtimeManifest = this.runtimeBundleService.getRuntimeManifest();
70
+ const runtimeRewritePlugin = createBrowserRuntimePlugin({
71
+ name: "react-renderer-runtime-import-rewrite",
72
+ manifest: runtimeManifest
55
73
  });
74
+ const runtimePlugins = bundleOptions.includeRuntime ? [] : [runtimeRewritePlugin].filter((plugin) => plugin !== null);
56
75
  if (isMdx && this.config.mdxCompilerOptions) {
57
- const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
58
76
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
59
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin];
77
+ options.plugins = [foreignJsxOverridePlugin, graphBoundaryPlugin, ...runtimePlugins, mdxPlugin];
60
78
  } else {
61
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, useSyncExternalStoreShimPlugin];
79
+ options.plugins = [foreignJsxOverridePlugin, graphBoundaryPlugin, ...runtimePlugins];
62
80
  }
63
81
  return options;
64
82
  }
65
- /**
66
- * Creates the esbuild plugin that rewrites bare React specifiers
67
- * to their runtime asset URLs.
68
- */
69
- createRuntimeAliasPlugin(runtimeSpecifierMap) {
70
- return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: "react-runtime-import-alias" });
71
- }
72
83
  }
73
84
  export {
74
85
  ReactBundleService
@@ -6,12 +6,21 @@
6
6
  */
7
7
  export declare class ReactHmrPageMetadataCache {
8
8
  private readonly declaredModulesByEntrypoint;
9
+ private readonly ownedEntrypoints;
10
+ /**
11
+ * Marks an HMR entrypoint as React-owned.
12
+ */
13
+ markOwnedEntrypoint(entrypointPath: string): void;
9
14
  /**
10
15
  * Stores the declared browser modules for a page entrypoint.
11
16
  */
12
- setDeclaredModules(entrypointPath: string, declaredModules: string[]): void;
17
+ setDeclaredModules(entrypointPath: string, declaredModules: readonly string[]): void;
13
18
  /**
14
19
  * Returns the last known declared browser modules for a page entrypoint.
15
20
  */
16
21
  getDeclaredModules(entrypointPath: string): string[] | undefined;
22
+ /**
23
+ * Returns true when the watched entrypoint is owned by the React integration.
24
+ */
25
+ ownsEntrypoint(entrypointPath: string): boolean;
17
26
  }
@@ -1,18 +1,34 @@
1
+ import path from "node:path";
1
2
  class ReactHmrPageMetadataCache {
2
3
  declaredModulesByEntrypoint = /* @__PURE__ */ new Map();
4
+ ownedEntrypoints = /* @__PURE__ */ new Set();
5
+ /**
6
+ * Marks an HMR entrypoint as React-owned.
7
+ */
8
+ markOwnedEntrypoint(entrypointPath) {
9
+ this.ownedEntrypoints.add(path.resolve(entrypointPath));
10
+ }
3
11
  /**
4
12
  * Stores the declared browser modules for a page entrypoint.
5
13
  */
6
14
  setDeclaredModules(entrypointPath, declaredModules) {
7
- this.declaredModulesByEntrypoint.set(entrypointPath, [...declaredModules]);
15
+ const resolvedEntrypointPath = path.resolve(entrypointPath);
16
+ this.markOwnedEntrypoint(resolvedEntrypointPath);
17
+ this.declaredModulesByEntrypoint.set(resolvedEntrypointPath, [...declaredModules]);
8
18
  }
9
19
  /**
10
20
  * Returns the last known declared browser modules for a page entrypoint.
11
21
  */
12
22
  getDeclaredModules(entrypointPath) {
13
- const declaredModules = this.declaredModulesByEntrypoint.get(entrypointPath);
23
+ const declaredModules = this.declaredModulesByEntrypoint.get(path.resolve(entrypointPath));
14
24
  return declaredModules ? [...declaredModules] : void 0;
15
25
  }
26
+ /**
27
+ * Returns true when the watched entrypoint is owned by the React integration.
28
+ */
29
+ ownsEntrypoint(entrypointPath) {
30
+ return this.ownedEntrypoints.has(path.resolve(entrypointPath));
31
+ }
16
32
  }
17
33
  export {
18
34
  ReactHmrPageMetadataCache
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * Hydration asset creation service for React integration.
3
3
  *
4
- * Builds the asset definitions (bundled component scripts + hydration bootstrap scripts)
5
- * required for client-side React rendering — both at the page level and the component
6
- * island level.
4
+ * Builds the asset definitions required for client-side React rendering both at
5
+ * the page level and the component island level.
7
6
  *
8
7
  * @module
9
8
  */
@@ -23,53 +22,63 @@ export interface ReactHydrationAssetServiceConfig {
23
22
  bundleService: ReactBundleService;
24
23
  hmrPageMetadataCache?: ReactHmrPageMetadataCache;
25
24
  }
25
+ export declare function getReactIslandComponentKey(componentFile: string, config?: EcoComponentConfig): string;
26
26
  /**
27
27
  * Manages the creation of client-side hydration assets for React pages and component islands.
28
28
  */
29
29
  export declare class ReactHydrationAssetService {
30
30
  private readonly config;
31
+ private static readonly ROUTER_PAGE_GROUPED_BUNDLE_ID;
31
32
  constructor(config: ReactHydrationAssetServiceConfig);
33
+ private getIslandBundleName;
34
+ private getIslandHydrationName;
35
+ private getRouterPageGroupedEntryName;
32
36
  /**
33
- * Resolves the import path for the bundled page component.
37
+ * Resolves the browser import path used for a React-owned page or island module.
34
38
  * Uses HMR manager for development or constructs static path for production.
35
39
  *
36
40
  * @param pagePath - Absolute path to the page source file
37
- * @param componentName - Generated unique component name
38
- * @returns The resolved import path for the bundled component
41
+ * @param assetName - Generated asset name
42
+ * @returns The resolved browser import path for the module
39
43
  */
40
- resolveAssetImportPath(pagePath: string, componentName: string): Promise<string>;
44
+ resolveAssetImportPath(pagePath: string, assetName: string): Promise<string>;
41
45
  /**
42
- * Creates the asset dependencies for a page: the bundled component and hydration script.
46
+ * Creates the page-owned route entry asset for hydration and client navigation.
43
47
  *
44
48
  * @param pagePath - Absolute path to the page source file
45
49
  * @param componentName - Generated unique component name
46
- * @param importPath - Resolved import path for the bundled component
50
+ * @param importPath - Resolved browser import path used by development HMR
47
51
  * @param bundleOptions - Bundle configuration options
48
52
  * @param isDevelopment - Whether running in development mode with HMR
49
53
  * @param isMdx - Whether the source file is an MDX file
50
- * @param props - Optional page props for client serialization
51
- * @returns Array of asset definitions for processing
54
+ * @returns One page-owned asset definition for processing
52
55
  */
53
- createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, isMdx: boolean, props?: Record<string, unknown>): AssetDefinition[];
56
+ createPageDependencies(pagePath: string, componentName: string, importPath: string, pageModuleUrlExpression: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, useBrowserRuntimeImports: boolean, isMdx: boolean): AssetDefinition[];
54
57
  /**
55
58
  * Builds client-side assets for a React component island.
56
59
  *
57
- * Includes the bundled component entry and an inline hydration bootstrap script.
60
+ * Includes the bundled component entry and a shared hydration bootstrap script.
58
61
  *
59
62
  * @param componentFile - Absolute path to the component source file
60
- * @param componentInstanceId - Unique instance ID for DOM targeting
61
- * @param props - Serialized props for client-side hydration
62
63
  * @param config - Optional component config with `__eco` metadata
63
64
  * @returns Processed assets ready for injection
64
65
  */
65
- buildComponentRenderAssets(componentFile: string, componentInstanceId: string, props: Record<string, unknown>, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
66
+ buildComponentRenderAssets(componentFile: string, config?: EcoComponentConfig): Promise<ProcessedAsset[]>;
66
67
  /**
67
- * Builds all client-side route assets for a page.
68
+ * Creates the Page Browser Graph dependency declarations for a React page.
68
69
  *
69
70
  * @param pagePath - Absolute file path of the page
70
71
  * @param isMdx - Whether the page is an MDX file
71
72
  * @param declaredModules - Explicitly declared browser module specifiers
72
- * @returns Processed assets for the route
73
+ * @returns Declarative assets for core-owned processing
74
+ */
75
+ createPageBrowserGraphDependencies(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<AssetDefinition[]>;
76
+ /**
77
+ * Builds the Page Browser Graph assets for a React page.
78
+ *
79
+ * @remarks
80
+ * Kept as a compatibility wrapper while callers migrate to core-owned page
81
+ * graph assembly.
73
82
  */
74
- buildRouteRenderAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
83
+ buildPageBrowserGraphAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
75
84
  }
@@ -6,109 +6,106 @@ import {
6
6
  } from "@ecopages/core/services/asset-processing-service";
7
7
  import { createHydrationScript, createIslandHydrationScript } from "../utils/hydration-scripts.js";
8
8
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
9
+ function getReactIslandComponentKey(componentFile, config) {
10
+ return rapidhash(`${componentFile}:${config?.__eco?.id ?? ""}`).toString();
11
+ }
9
12
  class ReactHydrationAssetService {
10
13
  config;
14
+ static ROUTER_PAGE_GROUPED_BUNDLE_ID = "ecopages-react-router-pages";
11
15
  constructor(config) {
12
16
  this.config = config;
13
17
  }
18
+ getIslandBundleName(componentFile) {
19
+ return `ecopages-react-island-${rapidhash(componentFile)}`;
20
+ }
21
+ getIslandHydrationName(bundleName, componentKey) {
22
+ return `${bundleName}-hydration-${componentKey}`;
23
+ }
24
+ getRouterPageGroupedEntryName(pagePath) {
25
+ const relativePath = path.relative(this.config.srcDir, pagePath);
26
+ return relativePath.replace(/\.(tsx?|jsx?|mdx?)$/, "").replace(/[\\/]+/g, "__").replace(/\[([^\]]+)\]/g, "_$1_");
27
+ }
14
28
  /**
15
- * Resolves the import path for the bundled page component.
29
+ * Resolves the browser import path used for a React-owned page or island module.
16
30
  * Uses HMR manager for development or constructs static path for production.
17
31
  *
18
32
  * @param pagePath - Absolute path to the page source file
19
- * @param componentName - Generated unique component name
20
- * @returns The resolved import path for the bundled component
33
+ * @param assetName - Generated asset name
34
+ * @returns The resolved browser import path for the module
21
35
  */
22
- async resolveAssetImportPath(pagePath, componentName) {
36
+ async resolveAssetImportPath(pagePath, assetName) {
23
37
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
24
38
  if (hmrManager?.isEnabled()) {
25
39
  return hmrManager.registerEntrypoint(pagePath);
26
40
  }
27
- return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${componentName}.js`).replace(/\\/g, "/")}`;
41
+ return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${assetName}.js`).replace(/\\/g, "/")}`;
28
42
  }
29
43
  /**
30
- * Creates the asset dependencies for a page: the bundled component and hydration script.
44
+ * Creates the page-owned route entry asset for hydration and client navigation.
31
45
  *
32
46
  * @param pagePath - Absolute path to the page source file
33
47
  * @param componentName - Generated unique component name
34
- * @param importPath - Resolved import path for the bundled component
48
+ * @param importPath - Resolved browser import path used by development HMR
35
49
  * @param bundleOptions - Bundle configuration options
36
50
  * @param isDevelopment - Whether running in development mode with HMR
37
51
  * @param isMdx - Whether the source file is an MDX file
38
- * @param props - Optional page props for client serialization
39
- * @returns Array of asset definitions for processing
52
+ * @returns One page-owned asset definition for processing
40
53
  */
41
- createPageDependencies(pagePath, componentName, importPath, bundleOptions, isDevelopment, isMdx, props) {
54
+ createPageDependencies(pagePath, componentName, importPath, pageModuleUrlExpression, bundleOptions, isDevelopment, useBrowserRuntimeImports, isMdx) {
42
55
  const runtimeImports = this.config.bundleService.getRuntimeImports();
43
- const dependencies = [
44
- AssetFactory.createFileScript({
45
- position: "head",
46
- filepath: pagePath,
47
- name: componentName,
48
- excludeFromHtml: true,
49
- bundle: true,
50
- bundleOptions,
51
- attributes: {
52
- type: "module",
53
- defer: "",
54
- "data-eco-persist": "true"
55
- }
56
- })
57
- ];
58
- if (props && Object.keys(props).length > 0) {
59
- dependencies.push(
60
- AssetFactory.createContentScript({
61
- position: "head",
62
- content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
63
- name: `${componentName}-props`,
64
- bundle: false,
65
- attributes: {
66
- type: "module"
67
- }
68
- })
69
- );
70
- }
71
- dependencies.push(
56
+ const groupedBundle = this.config.routerAdapter ? {
57
+ id: ReactHydrationAssetService.ROUTER_PAGE_GROUPED_BUNDLE_ID,
58
+ entryName: this.getRouterPageGroupedEntryName(pagePath)
59
+ } : void 0;
60
+ return [
72
61
  AssetFactory.createContentScript({
73
62
  position: "head",
74
63
  content: createHydrationScript({
75
- importPath,
76
- reactImportPath: runtimeImports.react,
77
- reactDomClientImportPath: runtimeImports.reactDomClient,
78
- routerImportPath: runtimeImports.router,
64
+ importPath: isDevelopment ? importPath : pagePath,
65
+ pageModuleUrlExpression,
66
+ reactImportPath: useBrowserRuntimeImports ? runtimeImports.react : "react",
67
+ reactDomClientImportPath: useBrowserRuntimeImports ? runtimeImports.reactDomClient : "react-dom/client",
68
+ routerImportPath: useBrowserRuntimeImports ? runtimeImports.router : this.config.routerAdapter?.bundle.importPath,
79
69
  isDevelopment,
80
70
  isMdx,
81
- router: this.config.routerAdapter
71
+ router: this.config.routerAdapter,
72
+ scriptId: componentName
82
73
  }),
83
- name: `${componentName}-hydration`,
84
- bundle: false,
74
+ name: componentName,
75
+ packageRole: "page-script",
76
+ bundle: !isDevelopment,
77
+ groupedBundle,
78
+ bundleOptions,
85
79
  attributes: {
86
80
  type: "module",
87
81
  defer: "",
88
82
  "data-eco-rerun": "true",
89
- "data-eco-script-id": `${componentName}-hydration`,
83
+ "data-eco-script-id": componentName,
84
+ ...this.config.routerAdapter ? { "data-eco-page-bootstrap": "react-router" } : {},
90
85
  "data-eco-persist": "true"
91
86
  }
92
87
  })
93
- );
94
- return dependencies;
88
+ ];
95
89
  }
96
90
  /**
97
91
  * Builds client-side assets for a React component island.
98
92
  *
99
- * Includes the bundled component entry and an inline hydration bootstrap script.
93
+ * Includes the bundled component entry and a shared hydration bootstrap script.
100
94
  *
101
95
  * @param componentFile - Absolute path to the component source file
102
- * @param componentInstanceId - Unique instance ID for DOM targeting
103
- * @param props - Serialized props for client-side hydration
104
96
  * @param config - Optional component config with `__eco` metadata
105
97
  * @returns Processed assets ready for injection
106
98
  */
107
- async buildComponentRenderAssets(componentFile, componentInstanceId, props, config) {
108
- const componentName = `ecopages-react-island-${rapidhash(`${componentFile}:${componentInstanceId}`)}`;
109
- const importPath = await this.resolveAssetImportPath(componentFile, componentName);
99
+ async buildComponentRenderAssets(componentFile, config) {
100
+ const componentName = this.getIslandBundleName(componentFile);
101
+ const componentKey = getReactIslandComponentKey(componentFile, config);
102
+ const hydrationName = this.getIslandHydrationName(componentName, componentKey);
110
103
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
111
104
  const isDevelopment = hmrManager?.isEnabled() ?? false;
105
+ if (isDevelopment) {
106
+ this.config.hmrPageMetadataCache?.markOwnedEntrypoint(componentFile);
107
+ }
108
+ const importPath = await this.resolveAssetImportPath(componentFile, componentName);
112
109
  const declaredModules = collectDeclaredModulesInConfig(config);
113
110
  const bundleOptions = await this.config.bundleService.createBundleOptions(
114
111
  componentName,
@@ -121,6 +118,7 @@ class ReactHydrationAssetService {
121
118
  position: "head",
122
119
  filepath: componentFile,
123
120
  name: componentName,
121
+ packageRole: "dynamic-chunk",
124
122
  excludeFromHtml: true,
125
123
  bundle: true,
126
124
  bundleOptions,
@@ -134,21 +132,22 @@ class ReactHydrationAssetService {
134
132
  position: "head",
135
133
  content: createIslandHydrationScript({
136
134
  importPath,
135
+ scriptId: hydrationName,
137
136
  reactImportPath: runtimeImports.react,
138
137
  reactDomClientImportPath: runtimeImports.reactDomClient,
139
- targetSelector: `[data-eco-component-id="${componentInstanceId}"]`,
140
- props,
138
+ targetSelector: `[data-eco-component-key="${componentKey}"]`,
141
139
  componentRef: config?.__eco?.id,
142
140
  componentFile,
143
141
  isDevelopment
144
142
  }),
145
- name: `${componentName}-hydration`,
143
+ name: hydrationName,
144
+ packageRole: "keep-separate",
146
145
  bundle: false,
147
146
  attributes: {
148
147
  type: "module",
149
148
  defer: "",
150
149
  "data-eco-rerun": "true",
151
- "data-eco-script-id": `${componentName}-hydration`,
150
+ "data-eco-script-id": hydrationName,
152
151
  "data-eco-persist": "true"
153
152
  }
154
153
  })
@@ -159,34 +158,53 @@ class ReactHydrationAssetService {
159
158
  return this.config.assetProcessingService.processDependencies(dependencies, componentName);
160
159
  }
161
160
  /**
162
- * Builds all client-side route assets for a page.
161
+ * Creates the Page Browser Graph dependency declarations for a React page.
163
162
  *
164
163
  * @param pagePath - Absolute file path of the page
165
164
  * @param isMdx - Whether the page is an MDX file
166
165
  * @param declaredModules - Explicitly declared browser module specifiers
167
- * @returns Processed assets for the route
166
+ * @returns Declarative assets for core-owned processing
168
167
  */
169
- async buildRouteRenderAssets(pagePath, isMdx, declaredModules) {
168
+ async createPageBrowserGraphDependencies(pagePath, isMdx, declaredModules) {
170
169
  const componentName = `ecopages-react-${rapidhash(pagePath)}`;
171
170
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
172
171
  const isDevelopment = hmrManager?.isEnabled() ?? false;
172
+ const isHostedDevelopment = !isDevelopment && process.env.NODE_ENV !== "production";
173
+ const usesRouterRuntime = Boolean(this.config.routerAdapter);
174
+ const useBrowserRuntimeImports = isDevelopment || isHostedDevelopment || usesRouterRuntime;
173
175
  if (isDevelopment) {
174
176
  this.config.hmrPageMetadataCache?.setDeclaredModules(pagePath, declaredModules);
175
177
  }
176
178
  const importPath = await this.resolveAssetImportPath(pagePath, componentName);
179
+ const pageModuleUrlExpression = isDevelopment ? JSON.stringify(importPath) : "import.meta.url";
177
180
  const bundleOptions = await this.config.bundleService.createBundleOptions(
178
181
  componentName,
179
182
  isMdx,
180
- declaredModules
183
+ declaredModules,
184
+ { includeRuntime: !useBrowserRuntimeImports, splitting: usesRouterRuntime }
181
185
  );
182
186
  const dependencies = this.createPageDependencies(
183
187
  pagePath,
184
188
  componentName,
185
189
  importPath,
190
+ pageModuleUrlExpression,
186
191
  bundleOptions,
187
192
  isDevelopment,
193
+ useBrowserRuntimeImports,
188
194
  isMdx
189
195
  );
196
+ return dependencies;
197
+ }
198
+ /**
199
+ * Builds the Page Browser Graph assets for a React page.
200
+ *
201
+ * @remarks
202
+ * Kept as a compatibility wrapper while callers migrate to core-owned page
203
+ * graph assembly.
204
+ */
205
+ async buildPageBrowserGraphAssets(pagePath, isMdx, declaredModules) {
206
+ const componentName = `ecopages-react-${rapidhash(pagePath)}`;
207
+ const dependencies = await this.createPageBrowserGraphDependencies(pagePath, isMdx, declaredModules);
190
208
  if (!this.config.assetProcessingService) {
191
209
  throw new Error("AssetProcessingService is not set");
192
210
  }
@@ -194,5 +212,6 @@ class ReactHydrationAssetService {
194
212
  }
195
213
  }
196
214
  export {
197
- ReactHydrationAssetService
215
+ ReactHydrationAssetService,
216
+ getReactIslandComponentKey
198
217
  };