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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +19 -39
  2. package/README.md +160 -18
  3. package/package.json +6 -6
  4. package/src/react-hmr-strategy.d.ts +26 -21
  5. package/src/react-hmr-strategy.js +91 -110
  6. package/src/react-renderer.d.ts +165 -41
  7. package/src/react-renderer.js +451 -158
  8. package/src/react.constants.d.ts +1 -0
  9. package/src/react.constants.js +4 -0
  10. package/src/react.plugin.d.ts +37 -108
  11. package/src/react.plugin.js +125 -54
  12. package/src/react.types.d.ts +88 -0
  13. package/src/react.types.js +0 -0
  14. package/src/router-adapter.d.ts +2 -2
  15. package/src/services/react-bundle.service.d.ts +4 -25
  16. package/src/services/react-bundle.service.js +39 -91
  17. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  18. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  19. package/src/services/react-hydration-asset.service.d.ts +7 -6
  20. package/src/services/react-hydration-asset.service.js +29 -17
  21. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  22. package/src/services/react-mdx-config-dependency.service.js +122 -0
  23. package/src/services/react-page-module.service.d.ts +8 -2
  24. package/src/services/react-page-module.service.js +44 -37
  25. package/src/services/react-page-payload.service.d.ts +46 -0
  26. package/src/services/react-page-payload.service.js +67 -0
  27. package/src/services/react-runtime-bundle.service.d.ts +14 -12
  28. package/src/services/react-runtime-bundle.service.js +103 -180
  29. package/src/utils/client-graph-boundary-plugin.js +149 -11
  30. package/src/utils/component-config-traversal.d.ts +36 -0
  31. package/src/utils/component-config-traversal.js +54 -0
  32. package/src/utils/declared-modules.d.ts +1 -1
  33. package/src/utils/declared-modules.js +7 -16
  34. package/src/utils/dynamic.test.browser.d.ts +1 -0
  35. package/src/utils/dynamic.test.browser.js +33 -0
  36. package/src/utils/hydration-scripts.d.ts +19 -4
  37. package/src/utils/hydration-scripts.js +102 -39
  38. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  39. package/src/utils/hydration-scripts.test.browser.js +126 -0
  40. package/src/utils/reachability-analyzer.d.ts +12 -1
  41. package/src/utils/reachability-analyzer.js +101 -5
  42. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  43. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  44. package/src/utils/react-mdx-loader-plugin.js +13 -5
  45. package/src/utils/react-runtime-specifier-map.d.ts +6 -0
  46. package/src/utils/react-runtime-specifier-map.js +37 -0
  47. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  48. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  49. package/src/react-hmr-strategy.ts +0 -444
  50. package/src/react-renderer.ts +0 -403
  51. package/src/react.plugin.ts +0 -241
  52. package/src/router-adapter.ts +0 -95
  53. package/src/services/react-bundle.service.ts +0 -212
  54. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  55. package/src/services/react-hydration-asset.service.ts +0 -260
  56. package/src/services/react-page-module.service.ts +0 -214
  57. package/src/services/react-runtime-bundle.service.ts +0 -271
  58. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  59. package/src/utils/client-only.ts +0 -27
  60. package/src/utils/declared-modules.ts +0 -99
  61. package/src/utils/dynamic.ts +0 -27
  62. package/src/utils/hmr-scripts.ts +0 -47
  63. package/src/utils/html-boundary.ts +0 -66
  64. package/src/utils/hydration-scripts.ts +0 -338
  65. package/src/utils/reachability-analyzer.ts +0 -440
  66. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,13 +1,23 @@
1
1
  import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
2
+ import {
3
+ buildReactRuntimeSpecifierMap,
4
+ getReactClientGraphAllowSpecifiers,
5
+ 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";
9
+ import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
2
10
  import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
3
11
  class ReactBundleService {
12
+ runtimeBundleService;
13
+ config;
4
14
  constructor(config) {
5
15
  this.config = config;
6
16
  this.runtimeBundleService = new ReactRuntimeBundleService({
17
+ rootDir: config.rootDir,
7
18
  routerAdapter: config.routerAdapter
8
19
  });
9
20
  }
10
- runtimeBundleService;
11
21
  /**
12
22
  * Returns resolved runtime import paths for the React runtime.
13
23
  */
@@ -24,8 +34,9 @@ class ReactBundleService {
24
34
  */
25
35
  async createBundleOptions(componentName, isMdx, declaredModules) {
26
36
  const runtimeImports = this.getRuntimeImports();
37
+ const runtimeSpecifierMap = buildReactRuntimeSpecifierMap(runtimeImports, this.config.routerAdapter);
27
38
  const options = {
28
- external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"],
39
+ external: getReactRuntimeExternalSpecifiers(),
29
40
  mainFields: ["module", "browser", "main"],
30
41
  naming: `${componentName}.[ext]`,
31
42
  ...import.meta.env?.NODE_ENV === "production" && {
@@ -37,24 +48,35 @@ class ReactBundleService {
37
48
  const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
38
49
  absWorkingDir: this.config.rootDir,
39
50
  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
- ]
51
+ alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
52
+ });
53
+ const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
54
+ name: "react-renderer-foreign-jsx-override",
55
+ hostJsxImportSource: this.config.jsxImportSource ?? "react",
56
+ foreignExtensions: this.config.nonReactExtensions ?? []
57
+ });
58
+ const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
59
+ const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
60
+ name: "react-renderer-use-sync-external-store-shim",
61
+ namespace: "ecopages-react-renderer-shim"
49
62
  });
50
- const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeImports);
51
- const useSyncExternalStoreShimPlugin = this.createSyncExternalStorePlugin();
52
63
  if (isMdx && this.config.mdxCompilerOptions) {
53
64
  const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
54
65
  const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
55
- options.plugins = [runtimeAliasPlugin, mdxPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
66
+ options.plugins = [
67
+ foreignJsxOverridePlugin,
68
+ graphBoundaryPlugin,
69
+ runtimeAliasPlugin,
70
+ mdxPlugin,
71
+ useSyncExternalStoreShimPlugin
72
+ ];
56
73
  } else {
57
- options.plugins = [runtimeAliasPlugin, useSyncExternalStoreShimPlugin, graphBoundaryPlugin];
74
+ options.plugins = [
75
+ foreignJsxOverridePlugin,
76
+ graphBoundaryPlugin,
77
+ runtimeAliasPlugin,
78
+ useSyncExternalStoreShimPlugin
79
+ ];
58
80
  }
59
81
  return options;
60
82
  }
@@ -62,82 +84,8 @@ class ReactBundleService {
62
84
  * Creates the esbuild plugin that rewrites bare React specifiers
63
85
  * to their runtime asset URLs.
64
86
  */
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
- * Creates the esbuild plugin that shims `use-sync-external-store/shim`
98
- * to re-export from React's built-in `useSyncExternalStore`.
99
- * This is needed because some packages use `use-sync-external-store/shim`
100
- * but React 18+ has built-in `useSyncExternalStore`.
101
- */
102
- createSyncExternalStorePlugin() {
103
- return {
104
- name: "react-renderer-use-sync-external-store-shim",
105
- setup(build) {
106
- build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
107
- path: "use-sync-external-store/shim",
108
- namespace: "ecopages-react-renderer-shim"
109
- }));
110
- build.onLoad(
111
- { filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-renderer-shim" },
112
- () => ({
113
- contents: "export { useSyncExternalStore } from 'react';",
114
- loader: "js"
115
- })
116
- );
117
- build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
118
- contents: "export { useSyncExternalStore } from 'react';",
119
- loader: "js"
120
- }));
121
- build.onLoad(
122
- {
123
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
124
- },
125
- () => ({
126
- contents: "export { useSyncExternalStore } from 'react';",
127
- loader: "js"
128
- })
129
- );
130
- build.onLoad(
131
- {
132
- filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/
133
- },
134
- () => ({
135
- contents: "export { useSyncExternalStore } from 'react';",
136
- loader: "js"
137
- })
138
- );
139
- }
140
- };
87
+ createRuntimeAliasPlugin(runtimeSpecifierMap) {
88
+ return createRuntimeSpecifierAliasPlugin(runtimeSpecifierMap, { name: "react-runtime-import-alias" });
141
89
  }
142
90
  }
143
91
  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
@@ -23,21 +23,24 @@ export interface ReactHydrationAssetServiceConfig {
23
23
  bundleService: ReactBundleService;
24
24
  hmrPageMetadataCache?: ReactHmrPageMetadataCache;
25
25
  }
26
+ export declare function getReactIslandComponentKey(componentFile: string, config?: EcoComponentConfig): string;
26
27
  /**
27
28
  * Manages the creation of client-side hydration assets for React pages and component islands.
28
29
  */
29
30
  export declare class ReactHydrationAssetService {
30
31
  private readonly config;
31
32
  constructor(config: ReactHydrationAssetServiceConfig);
33
+ private getIslandBundleName;
34
+ private getIslandHydrationName;
32
35
  /**
33
36
  * Resolves the import path for the bundled page component.
34
37
  * Uses HMR manager for development or constructs static path for production.
35
38
  *
36
39
  * @param pagePath - Absolute path to the page source file
37
- * @param componentName - Generated unique component name
40
+ * @param assetName - Generated asset name
38
41
  * @returns The resolved import path for the bundled component
39
42
  */
40
- resolveAssetImportPath(pagePath: string, componentName: string): Promise<string>;
43
+ resolveAssetImportPath(pagePath: string, assetName: string): Promise<string>;
41
44
  /**
42
45
  * Creates the asset dependencies for a page: the bundled component and hydration script.
43
46
  *
@@ -54,15 +57,13 @@ export declare class ReactHydrationAssetService {
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
68
  * Builds all client-side route assets for a page.
68
69
  *
@@ -4,27 +4,36 @@ 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;
11
14
  constructor(config) {
12
15
  this.config = config;
13
16
  }
17
+ getIslandBundleName(componentFile) {
18
+ return `ecopages-react-island-${rapidhash(componentFile)}`;
19
+ }
20
+ getIslandHydrationName(bundleName, componentKey) {
21
+ return `${bundleName}-hydration-${componentKey}`;
22
+ }
14
23
  /**
15
24
  * Resolves the import path for the bundled page component.
16
25
  * Uses HMR manager for development or constructs static path for production.
17
26
  *
18
27
  * @param pagePath - Absolute path to the page source file
19
- * @param componentName - Generated unique component name
28
+ * @param assetName - Generated asset name
20
29
  * @returns The resolved import path for the bundled component
21
30
  */
22
- async resolveAssetImportPath(pagePath, componentName) {
31
+ async resolveAssetImportPath(pagePath, assetName) {
23
32
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
24
33
  if (hmrManager?.isEnabled()) {
25
34
  return hmrManager.registerEntrypoint(pagePath);
26
35
  }
27
- return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${componentName}.js`).replace(/\\/g, "/")}`;
36
+ return `/${path.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath)).replace(path.basename(pagePath), `${assetName}.js`).replace(/\\/g, "/")}`;
28
37
  }
29
38
  /**
30
39
  * Creates the asset dependencies for a page: the bundled component and hydration script.
@@ -59,7 +68,7 @@ class ReactHydrationAssetService {
59
68
  dependencies.push(
60
69
  AssetFactory.createContentScript({
61
70
  position: "head",
62
- content: `window.__ECO_PAGE__={module:"${importPath}",props:${JSON.stringify(props)}};`,
71
+ content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
63
72
  name: `${componentName}-props`,
64
73
  bundle: false,
65
74
  attributes: {
@@ -96,19 +105,22 @@ class ReactHydrationAssetService {
96
105
  /**
97
106
  * Builds client-side assets for a React component island.
98
107
  *
99
- * Includes the bundled component entry and an inline hydration bootstrap script.
108
+ * Includes the bundled component entry and a shared hydration bootstrap script.
100
109
  *
101
110
  * @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
111
  * @param config - Optional component config with `__eco` metadata
105
112
  * @returns Processed assets ready for injection
106
113
  */
107
- async buildComponentRenderAssets(componentFile, componentInstanceId, props, config) {
108
- const componentName = `ecopages-react-island-${rapidhash(`${componentFile}:${componentInstanceId}`)}`;
109
- const importPath = await this.resolveAssetImportPath(componentFile, componentName);
114
+ async buildComponentRenderAssets(componentFile, config) {
115
+ const componentName = this.getIslandBundleName(componentFile);
116
+ const componentKey = getReactIslandComponentKey(componentFile, config);
117
+ const hydrationName = this.getIslandHydrationName(componentName, componentKey);
110
118
  const hmrManager = this.config.assetProcessingService?.getHmrManager();
111
119
  const isDevelopment = hmrManager?.isEnabled() ?? false;
120
+ if (isDevelopment) {
121
+ this.config.hmrPageMetadataCache?.markOwnedEntrypoint(componentFile);
122
+ }
123
+ const importPath = await this.resolveAssetImportPath(componentFile, componentName);
112
124
  const declaredModules = collectDeclaredModulesInConfig(config);
113
125
  const bundleOptions = await this.config.bundleService.createBundleOptions(
114
126
  componentName,
@@ -136,19 +148,18 @@ class ReactHydrationAssetService {
136
148
  importPath,
137
149
  reactImportPath: runtimeImports.react,
138
150
  reactDomClientImportPath: runtimeImports.reactDomClient,
139
- targetSelector: `[data-eco-component-id="${componentInstanceId}"]`,
140
- props,
151
+ targetSelector: `[data-eco-component-key="${componentKey}"]`,
141
152
  componentRef: config?.__eco?.id,
142
153
  componentFile,
143
154
  isDevelopment
144
155
  }),
145
- name: `${componentName}-hydration`,
156
+ name: hydrationName,
146
157
  bundle: false,
147
158
  attributes: {
148
159
  type: "module",
149
160
  defer: "",
150
161
  "data-eco-rerun": "true",
151
- "data-eco-script-id": `${componentName}-hydration`,
162
+ "data-eco-script-id": hydrationName,
152
163
  "data-eco-persist": "true"
153
164
  }
154
165
  })
@@ -194,5 +205,6 @@ class ReactHydrationAssetService {
194
205
  }
195
206
  }
196
207
  export {
197
- ReactHydrationAssetService
208
+ ReactHydrationAssetService,
209
+ getReactIslandComponentKey
198
210
  };
@@ -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 {};
@@ -0,0 +1,122 @@
1
+ import path from "node:path";
2
+ import { rapidhash } from "@ecopages/core/hash";
3
+ import {
4
+ AssetFactory
5
+ } from "@ecopages/core/services/asset-processing-service";
6
+ import { collectFromConfigForest, getComponentConfigs } from "../utils/component-config-traversal.js";
7
+ class ReactMdxConfigDependencyService {
8
+ config;
9
+ constructor(config) {
10
+ this.config = config;
11
+ }
12
+ /**
13
+ * Processes MDX-owned config dependencies and eagerly emits any SSR-marked lazy scripts.
14
+ */
15
+ async processMdxConfigDependencies(options) {
16
+ const components = this.createOwnedConfigComponents(options.pagePath, options.config);
17
+ if (components.length === 0) {
18
+ return [];
19
+ }
20
+ const processedDependencies = await options.processComponentDependencies(components);
21
+ const eagerSsrLazyDependencies = await this.processDeclaredSsrLazyDependencies(components, options.pagePath);
22
+ return [...processedDependencies, ...eagerSsrLazyDependencies];
23
+ }
24
+ createOwnedConfigComponents(pagePath, config) {
25
+ const components = [];
26
+ const resolvedLayout = config?.layout;
27
+ if (resolvedLayout?.config?.dependencies) {
28
+ const layoutConfig = this.config.pageModuleService.ensureConfigFileMetadata(
29
+ resolvedLayout.config,
30
+ pagePath
31
+ );
32
+ components.push({ config: layoutConfig });
33
+ }
34
+ if (config?.dependencies) {
35
+ components.push({
36
+ config: {
37
+ ...config,
38
+ __eco: {
39
+ id: rapidhash(pagePath).toString(36),
40
+ file: pagePath,
41
+ integration: this.config.integrationName
42
+ }
43
+ }
44
+ });
45
+ }
46
+ return components;
47
+ }
48
+ async processDeclaredSsrLazyDependencies(components, pagePath) {
49
+ if (!this.config.assetProcessingService?.processDependencies) {
50
+ return [];
51
+ }
52
+ const dependencies = this.collectDeclaredSsrLazyDependencies(components);
53
+ if (dependencies.length === 0) {
54
+ return [];
55
+ }
56
+ return this.config.assetProcessingService.processDependencies(
57
+ dependencies,
58
+ `${this.config.integrationName}-mdx-ssr-lazy:${pagePath}`
59
+ );
60
+ }
61
+ /**
62
+ * Collects `lazy` script dependencies that also opt into SSR from an MDX config graph.
63
+ */
64
+ collectDeclaredSsrLazyDependencies(components) {
65
+ const dependencies = [];
66
+ const seenKeys = /* @__PURE__ */ new Set();
67
+ const normalizeAttributes = (attributes) => ({
68
+ type: "module",
69
+ defer: "",
70
+ ...attributes ?? {}
71
+ });
72
+ collectFromConfigForest(getComponentConfigs(components), (config) => {
73
+ const componentFile = config.__eco?.file;
74
+ if (!componentFile) {
75
+ return [];
76
+ }
77
+ const componentDir = path.dirname(componentFile);
78
+ for (const script of config.dependencies?.scripts ?? []) {
79
+ if (typeof script === "string" || !script.lazy || script.ssr !== true) {
80
+ continue;
81
+ }
82
+ const attributes = normalizeAttributes(script.attributes);
83
+ if (script.content) {
84
+ const key2 = `content:${script.content}:${JSON.stringify(attributes)}`;
85
+ if (seenKeys.has(key2)) {
86
+ continue;
87
+ }
88
+ seenKeys.add(key2);
89
+ dependencies.push(
90
+ AssetFactory.createContentScript({
91
+ position: "head",
92
+ content: script.content,
93
+ attributes
94
+ })
95
+ );
96
+ continue;
97
+ }
98
+ if (!script.src) {
99
+ continue;
100
+ }
101
+ const resolvedPath = path.resolve(componentDir, script.src);
102
+ const key = `file:${resolvedPath}:${JSON.stringify(attributes)}`;
103
+ if (seenKeys.has(key)) {
104
+ continue;
105
+ }
106
+ seenKeys.add(key);
107
+ dependencies.push(
108
+ AssetFactory.createFileScript({
109
+ filepath: resolvedPath,
110
+ position: "head",
111
+ attributes
112
+ })
113
+ );
114
+ }
115
+ return [];
116
+ });
117
+ return dependencies;
118
+ }
119
+ }
120
+ export {
121
+ ReactMdxConfigDependencyService
122
+ };
@@ -7,6 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
10
+ import type { BuildExecutor } from '@ecopages/core/build/build-adapter';
10
11
  import type { CompileOptions } from '@mdx-js/mdx';
11
12
  /**
12
13
  * Configuration for the ReactPageModuleService.
@@ -14,6 +15,8 @@ import type { CompileOptions } from '@mdx-js/mdx';
14
15
  export interface ReactPageModuleServiceConfig {
15
16
  rootDir: string;
16
17
  distDir: string;
18
+ workDir: string;
19
+ buildExecutor: BuildExecutor;
17
20
  layoutsDir?: string;
18
21
  componentsDir?: string;
19
22
  mdxCompilerOptions?: CompileOptions;
@@ -40,7 +43,10 @@ export declare class ReactPageModuleService {
40
43
  * @param filePath - Absolute path to the MDX file
41
44
  * @returns The imported module
42
45
  */
43
- importMdxPageFile(filePath: string): Promise<unknown>;
46
+ importMdxPageFile(filePath: string, options?: {
47
+ bypassCache?: boolean;
48
+ cacheScope?: string;
49
+ }): Promise<unknown>;
44
50
  /**
45
51
  * Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
46
52
  * Resolves the file path from dependency declarations when not already set.
@@ -54,7 +60,7 @@ export declare class ReactPageModuleService {
54
60
  * Recursively checks whether a component config tree declares any browser modules.
55
61
  * Used to determine if a page needs hydration.
56
62
  */
57
- hasModulesInConfig(config: EcoComponentConfig | undefined, visited?: Set<EcoComponentConfig>): boolean;
63
+ hasModulesInConfig(config: EcoComponentConfig | undefined): boolean;
58
64
  /**
59
65
  * Determines whether a page needs client-side hydration.
60
66
  *