@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.50

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 (68) hide show
  1. package/README.md +152 -29
  2. package/package.json +16 -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 +60 -43
  6. package/src/react-hmr-strategy.js +297 -144
  7. package/src/react-renderer.d.ts +169 -42
  8. package/src/react-renderer.js +484 -164
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +38 -111
  12. package/src/react.plugin.js +132 -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/services/react-bundle.service.d.ts +19 -31
  17. package/src/services/react-bundle.service.js +51 -100
  18. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  19. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  20. package/src/services/react-hydration-asset.service.d.ts +28 -19
  21. package/src/services/react-hydration-asset.service.js +85 -66
  22. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  23. package/src/services/react-mdx-config-dependency.service.js +122 -0
  24. package/src/services/react-page-module.service.d.ts +10 -2
  25. package/src/services/react-page-module.service.js +47 -39
  26. package/src/services/react-page-payload.service.d.ts +46 -0
  27. package/src/services/react-page-payload.service.js +67 -0
  28. package/src/services/react-runtime-bundle.service.d.ts +15 -13
  29. package/src/services/react-runtime-bundle.service.js +103 -180
  30. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  31. package/src/utils/client-graph-boundary-plugin.js +80 -3
  32. package/src/utils/component-config-traversal.d.ts +36 -0
  33. package/src/utils/component-config-traversal.js +54 -0
  34. package/src/utils/declared-modules.d.ts +1 -1
  35. package/src/utils/declared-modules.js +7 -16
  36. package/src/utils/dynamic.test.browser.d.ts +1 -0
  37. package/src/utils/dynamic.test.browser.js +33 -0
  38. package/src/utils/hydration-scripts.d.ts +27 -6
  39. package/src/utils/hydration-scripts.js +177 -44
  40. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  41. package/src/utils/hydration-scripts.test.browser.js +198 -0
  42. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  43. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  44. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  45. package/src/utils/react-mdx-loader-plugin.js +13 -5
  46. package/src/utils/react-runtime-alias-map.d.ts +6 -0
  47. package/src/utils/react-runtime-alias-map.js +33 -0
  48. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  49. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  50. package/CHANGELOG.md +0 -67
  51. package/src/react-hmr-strategy.ts +0 -455
  52. package/src/react-renderer.ts +0 -403
  53. package/src/react.plugin.ts +0 -241
  54. package/src/router-adapter.ts +0 -95
  55. package/src/services/react-bundle.service.ts +0 -217
  56. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  57. package/src/services/react-hydration-asset.service.ts +0 -260
  58. package/src/services/react-page-module.service.ts +0 -214
  59. package/src/services/react-runtime-bundle.service.ts +0 -271
  60. package/src/utils/client-graph-boundary-plugin.ts +0 -710
  61. package/src/utils/client-only.ts +0 -27
  62. package/src/utils/declared-modules.ts +0 -99
  63. package/src/utils/dynamic.ts +0 -27
  64. package/src/utils/hmr-scripts.ts +0 -47
  65. package/src/utils/html-boundary.ts +0 -66
  66. package/src/utils/hydration-scripts.ts +0 -338
  67. package/src/utils/reachability-analyzer.ts +0 -593
  68. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,13 +1,24 @@
1
1
  import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
2
+ import {
3
+ buildReactRuntimeAliasMap,
4
+ getReactClientGraphAllowSpecifiers,
5
+ getReactRuntimeExternalSpecifiers
6
+ } from "../utils/react-runtime-alias-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";
9
+ import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
2
10
  import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
11
+ import { createReactMdxLoaderPlugin } from "../utils/react-mdx-loader-plugin.js";
3
12
  class ReactBundleService {
13
+ runtimeBundleService;
14
+ config;
4
15
  constructor(config) {
5
16
  this.config = config;
6
17
  this.runtimeBundleService = new ReactRuntimeBundleService({
18
+ rootDir: config.rootDir,
7
19
  routerAdapter: config.routerAdapter
8
20
  });
9
21
  }
10
- runtimeBundleService;
11
22
  /**
12
23
  * Returns resolved runtime import paths for the React runtime.
13
24
  */
@@ -22,39 +33,58 @@ class ReactBundleService {
22
33
  * @param declaredModules - Explicitly declared browser module specifiers
23
34
  * @returns Bundle options object for the build adapter
24
35
  */
25
- async createBundleOptions(componentName, isMdx, declaredModules) {
36
+ async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
26
37
  const runtimeImports = this.getRuntimeImports();
27
38
  const options = {
28
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"],
29
39
  mainFields: ["module", "browser", "main"],
30
40
  naming: `${componentName}.[ext]`,
31
41
  ...import.meta.env?.NODE_ENV === "production" && {
32
42
  minify: true,
33
- splitting: false,
34
43
  treeshaking: true
35
- }
44
+ },
45
+ ...bundleOptions.splitting === void 0 ? {} : { splitting: bundleOptions.splitting }
36
46
  };
47
+ if (!bundleOptions.includeRuntime) {
48
+ const reactRuntimeSpecifiers = new Set(getReactRuntimeExternalSpecifiers());
49
+ options.external = [
50
+ ...Object.values(runtimeImports).filter(
51
+ (specifier) => Boolean(specifier) && !reactRuntimeSpecifiers.has(
52
+ specifier
53
+ )
54
+ )
55
+ ];
56
+ }
37
57
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
38
58
  absWorkingDir: this.config.rootDir,
39
59
  declaredModules,
40
- alwaysAllowSpecifiers: [
41
- "@ecopages/core",
42
- "react",
43
- "react-dom",
44
- "react/jsx-runtime",
45
- "react/jsx-dev-runtime",
46
- "react-dom/client",
47
- ...this.config.routerAdapter ? [this.config.routerAdapter.importMapKey] : []
48
- ]
60
+ alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
61
+ });
62
+ const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
63
+ name: "react-renderer-foreign-jsx-override",
64
+ hostJsxImportSource: this.config.jsxImportSource ?? "react",
65
+ foreignExtensions: this.config.nonReactExtensions ?? []
49
66
  });
50
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeImports);
51
- const useSyncExternalStoreShimPlugin = this.createSyncExternalStorePlugin();
67
+ const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
68
+ name: "react-renderer-use-sync-external-store-shim",
69
+ namespace: "ecopages-react-renderer-shim"
70
+ });
71
+ const runtimePlugins = bundleOptions.includeRuntime ? [] : [this.createRuntimeAliasPlugin(buildReactRuntimeAliasMap(runtimeImports))];
52
72
  if (isMdx && this.config.mdxCompilerOptions) {
53
- const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
54
73
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
55
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin];
74
+ options.plugins = [
75
+ foreignJsxOverridePlugin,
76
+ graphBoundaryPlugin,
77
+ ...runtimePlugins,
78
+ mdxPlugin,
79
+ useSyncExternalStoreShimPlugin
80
+ ];
56
81
  } else {
57
- options.plugins = [graphBoundaryPlugin, runtimeAliasPlugin, useSyncExternalStoreShimPlugin];
82
+ options.plugins = [
83
+ foreignJsxOverridePlugin,
84
+ graphBoundaryPlugin,
85
+ ...runtimePlugins,
86
+ useSyncExternalStoreShimPlugin
87
+ ];
58
88
  }
59
89
  return options;
60
90
  }
@@ -62,87 +92,8 @@ class ReactBundleService {
62
92
  * Creates the esbuild plugin that rewrites bare React specifiers
63
93
  * to their runtime asset URLs.
64
94
  */
65
- createRuntimeAliasPlugin(runtimeImports) {
66
- const aliases = /* @__PURE__ */ new Map([
67
- ["react", runtimeImports.react],
68
- ["react-dom/client", runtimeImports.reactDomClient],
69
- ["react/jsx-runtime", runtimeImports.reactJsxRuntime],
70
- ["react/jsx-dev-runtime", runtimeImports.reactJsxDevRuntime],
71
- ["react-dom", runtimeImports.reactDom]
72
- ]);
73
- if (this.config.routerAdapter && runtimeImports.router) {
74
- aliases.set(this.config.routerAdapter.importMapKey, runtimeImports.router);
75
- }
76
- const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
77
- const pattern = new RegExp(
78
- `^(${Array.from(aliases.keys()).map((key) => escapeRegExp(key)).join("|")})$`
79
- );
80
- return {
81
- name: "react-runtime-import-alias",
82
- setup(build) {
83
- build.onResolve({ filter: pattern }, (args) => {
84
- const mappedPath = aliases.get(args.path);
85
- if (!mappedPath) {
86
- return void 0;
87
- }
88
- return {
89
- path: mappedPath,
90
- external: true
91
- };
92
- });
93
- }
94
- };
95
- }
96
- /**
97
- * Redirects `use-sync-external-store/shim` imports to React's built-in
98
- * `useSyncExternalStore`.
99
- *
100
- * Libraries like React Aria still list `use-sync-external-store` as a
101
- * dependency to support React 16/17. On React 18+ the `/shim` export is
102
- * already a pass-through, but without this plugin esbuild would bundle
103
- * the full CJS shim (including `process.env` branching) into the browser
104
- * bundle. The plugin short-circuits the resolution so only a single clean
105
- * ESM re-export is emitted.
106
- */
107
- createSyncExternalStorePlugin() {
108
- return {
109
- name: "react-renderer-use-sync-external-store-shim",
110
- setup(build) {
111
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
112
- path: "use-sync-external-store/shim",
113
- namespace: "ecopages-react-renderer-shim"
114
- }));
115
- build.onLoad(
116
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-renderer-shim" },
117
- () => ({
118
- contents: "export { useSyncExternalStore } from 'react';",
119
- loader: "js"
120
- })
121
- );
122
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
123
- contents: "export { useSyncExternalStore } from 'react';",
124
- loader: "js"
125
- }));
126
- build.onLoad(
127
- {
128
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
129
- },
130
- () => ({
131
- contents: "export { useSyncExternalStore } from 'react';",
132
- loader: "js"
133
- })
134
- );
135
- build.onLoad(
136
- {
137
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/
138
- },
139
- () => ({
140
- contents: "export { useSyncExternalStore } from 'react';",
141
- loader: "js"
142
- })
143
- );
144
- }
145
- };
95
+ createRuntimeAliasPlugin(runtimeAliasMap) {
96
+ return createRuntimeSpecifierAliasPlugin(runtimeAliasMap, { name: "react-runtime-import-alias" });
146
97
  }
147
98
  }
148
99
  export {
@@ -6,6 +6,11 @@
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
  */
@@ -14,4 +19,8 @@ export declare class ReactHmrPageMetadataCache {
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
  }
@@ -4,111 +4,108 @@ import { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
4
4
  import {
5
5
  AssetFactory
6
6
  } from "@ecopages/core/services/asset-processing-service";
7
- import { createHydrationScript } from "../utils/hydration-scripts.js";
8
- import { createIslandHydrationScript } from "../utils/hydration-scripts.js";
7
+ import { createHydrationScript, createIslandHydrationScript } from "../utils/hydration-scripts.js";
9
8
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
9
+ function getReactIslandComponentKey(componentFile, config) {
10
+ return rapidhash(`${componentFile}:${config?.__eco?.id ?? ""}`).toString();
11
+ }
10
12
  class ReactHydrationAssetService {
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_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
  };
@@ -0,0 +1,36 @@
1
+ import type { EcoComponent, EcoComponentConfig } from '@ecopages/core';
2
+ import { type AssetProcessingService, type ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
3
+ import type { ReactPageModuleService } from './react-page-module.service.js';
4
+ type MdxConfigDependencyProcessor = (components: Partial<EcoComponent>[]) => Promise<ProcessedAsset[]>;
5
+ export interface ReactMdxConfigDependencyServiceConfig {
6
+ integrationName: string;
7
+ pageModuleService: Pick<ReactPageModuleService, 'ensureConfigFileMetadata'>;
8
+ assetProcessingService?: Pick<AssetProcessingService, 'processDependencies'>;
9
+ }
10
+ /**
11
+ * Resolves MDX-owned config dependencies that live outside the normal React component tree.
12
+ *
13
+ * React MDX pages can declare dependencies on the page config itself or on a
14
+ * resolved layout config. Those roots need to be materialized as synthetic
15
+ * component configs so the shared dependency pipeline can process them without
16
+ * growing more MDX-specific logic inside the renderer.
17
+ */
18
+ export declare class ReactMdxConfigDependencyService {
19
+ private readonly config;
20
+ constructor(config: ReactMdxConfigDependencyServiceConfig);
21
+ /**
22
+ * Processes MDX-owned config dependencies and eagerly emits any SSR-marked lazy scripts.
23
+ */
24
+ processMdxConfigDependencies(options: {
25
+ pagePath: string;
26
+ config?: EcoComponentConfig;
27
+ processComponentDependencies: MdxConfigDependencyProcessor;
28
+ }): Promise<ProcessedAsset[]>;
29
+ private createOwnedConfigComponents;
30
+ private processDeclaredSsrLazyDependencies;
31
+ /**
32
+ * Collects `lazy` script dependencies that also opt into SSR from an MDX config graph.
33
+ */
34
+ private collectDeclaredSsrLazyDependencies;
35
+ }
36
+ export {};