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

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 +24 -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 +65 -43
  6. package/src/react-hmr-strategy.js +298 -145
  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 +40 -111
  12. package/src/react.plugin.js +136 -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/react-bundle.service.d.ts +22 -35
  19. package/src/services/react-bundle.service.js +41 -105
  20. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  21. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  22. package/src/services/react-hydration-asset.service.d.ts +28 -19
  23. package/src/services/react-hydration-asset.service.js +85 -66
  24. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  25. package/src/services/react-mdx-config-dependency.service.js +122 -0
  26. package/src/services/react-page-module.service.d.ts +10 -2
  27. package/src/services/react-page-module.service.js +47 -39
  28. package/src/services/react-page-payload.service.d.ts +46 -0
  29. package/src/services/react-page-payload.service.js +67 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +20 -13
  31. package/src/services/react-runtime-bundle.service.js +146 -179
  32. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  33. package/src/utils/client-graph-boundary-plugin.js +80 -3
  34. package/src/utils/component-config-traversal.d.ts +36 -0
  35. package/src/utils/component-config-traversal.js +54 -0
  36. package/src/utils/declared-modules.d.ts +1 -1
  37. package/src/utils/declared-modules.js +7 -16
  38. package/src/utils/dynamic.test.browser.d.ts +1 -0
  39. package/src/utils/dynamic.test.browser.js +33 -0
  40. package/src/utils/hydration-scripts.d.ts +27 -6
  41. package/src/utils/hydration-scripts.js +177 -44
  42. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  43. package/src/utils/hydration-scripts.test.browser.js +198 -0
  44. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  45. package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
  46. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  47. package/src/utils/react-mdx-loader-plugin.js +13 -5
  48. package/src/utils/react-runtime-alias-map.d.ts +8 -0
  49. package/src/utils/react-runtime-alias-map.js +90 -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
@@ -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 {};
@@ -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,12 @@ 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<EcoPageFile<{
50
+ config?: EcoComponentConfig;
51
+ }>>;
44
52
  /**
45
53
  * Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
46
54
  * Resolves the file path from dependency declarations when not already set.
@@ -54,7 +62,7 @@ export declare class ReactPageModuleService {
54
62
  * Recursively checks whether a component config tree declares any browser modules.
55
63
  * Used to determine if a page needs hydration.
56
64
  */
57
- hasModulesInConfig(config: EcoComponentConfig | undefined, visited?: Set<EcoComponentConfig>): boolean;
65
+ hasModulesInConfig(config: EcoComponentConfig | undefined): boolean;
58
66
  /**
59
67
  * Determines whether a page needs client-side hydration.
60
68
  *
@@ -1,10 +1,13 @@
1
1
  import path from "node:path";
2
2
  import { pathToFileURL } from "node:url";
3
3
  import { rapidhash } from "@ecopages/core/hash";
4
- import { defaultBuildAdapter } from "@ecopages/core/build/build-adapter";
4
+ import { build } from "@ecopages/core/build/build-adapter";
5
5
  import { fileSystem } from "@ecopages/file-system";
6
+ import { someInConfigTree } from "../utils/component-config-traversal.js";
6
7
  import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
8
+ import { createReactMdxLoaderPlugin } from "../utils/react-mdx-loader-plugin.js";
7
9
  class ReactPageModuleService {
10
+ config;
8
11
  constructor(config) {
9
12
  this.config = config;
10
13
  }
@@ -22,8 +25,7 @@ class ReactPageModuleService {
22
25
  * @param filePath - Absolute path to the MDX file
23
26
  * @returns The imported module
24
27
  */
25
- async importMdxPageFile(filePath) {
26
- const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
28
+ async importMdxPageFile(filePath, options) {
27
29
  const mdxPlugin = createReactMdxLoaderPlugin(
28
30
  this.config.mdxCompilerOptions ?? {
29
31
  jsxImportSource: "react",
@@ -31,34 +33,49 @@ class ReactPageModuleService {
31
33
  development: process?.env?.NODE_ENV === "development"
32
34
  }
33
35
  );
34
- const outdir = path.join(this.config.distDir, ".server-modules-react-mdx");
36
+ const outdir = path.join(this.config.workDir, ".server-modules-react-mdx");
35
37
  const fileBaseName = path.basename(filePath, path.extname(filePath));
36
38
  const fileHash = fileSystem.hash(filePath);
37
- const cacheBuster = process?.env?.NODE_ENV === "development" ? `-${Date.now()}` : "";
38
- const outputFileName = `${fileBaseName}-${fileHash}${cacheBuster}.js`;
39
- const buildResult = await defaultBuildAdapter.build({
40
- entrypoints: [filePath],
41
- root: this.config.rootDir,
42
- outdir,
43
- target: "node",
44
- format: "esm",
45
- sourcemap: "none",
46
- splitting: false,
47
- minify: false,
48
- treeshaking: false,
49
- naming: outputFileName,
50
- plugins: [mdxPlugin]
51
- });
39
+ const cacheScopeSuffix = options?.cacheScope ? `-${sanitizeCacheScope(options.cacheScope)}` : "";
40
+ const cacheBuster = options?.bypassCache || process?.env?.NODE_ENV === "development" ? `-${Date.now()}` : "";
41
+ const outputFileName = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${cacheBuster}.mjs`;
42
+ const outputNamingTemplate = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${cacheBuster}.[ext]`;
43
+ const buildResult = await build(
44
+ {
45
+ entrypoints: [filePath],
46
+ root: this.config.rootDir,
47
+ outdir,
48
+ target: "es2022",
49
+ format: "esm",
50
+ sourcemap: "none",
51
+ splitting: false,
52
+ minify: false,
53
+ treeshaking: false,
54
+ naming: outputNamingTemplate,
55
+ plugins: [mdxPlugin]
56
+ },
57
+ this.config.buildExecutor
58
+ );
52
59
  if (!buildResult.success) {
53
60
  const details = buildResult.logs.map((log) => log.message).join(" | ");
54
61
  throw new Error(`Failed to compile MDX page module: ${details}`);
55
62
  }
56
63
  const preferredOutputPath = path.join(outdir, outputFileName);
57
- const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => output.path.endsWith(".js"))?.path;
64
+ const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => /\.(?:[cm]?js)$/u.test(output.path))?.path;
58
65
  if (!compiledOutput) {
59
66
  throw new Error(`No compiled MDX output generated for page: ${filePath}`);
60
67
  }
61
- return await import(pathToFileURL(compiledOutput).href);
68
+ const compiledOutputUrl = pathToFileURL(compiledOutput);
69
+ if (process?.env?.NODE_ENV === "development" || options?.cacheScope) {
70
+ compiledOutputUrl.searchParams.set(
71
+ "update",
72
+ [fileHash, options?.cacheScope ? sanitizeCacheScope(options.cacheScope) : void 0].filter((value) => value !== void 0).join("-")
73
+ );
74
+ }
75
+ return await import(
76
+ /* @vite-ignore */
77
+ compiledOutputUrl.href
78
+ );
62
79
  }
63
80
  /**
64
81
  * Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
@@ -91,7 +108,7 @@ class ReactPageModuleService {
91
108
  if (fileSystem.exists(resolvedDependency)) {
92
109
  return {
93
110
  ...config,
94
- __eco: buildEcoMeta(resolvedDependency)
111
+ __eco: buildEcoMeta(path.join(candidateDir, path.basename(pagePath)))
95
112
  };
96
113
  }
97
114
  }
@@ -105,23 +122,11 @@ class ReactPageModuleService {
105
122
  * Recursively checks whether a component config tree declares any browser modules.
106
123
  * Used to determine if a page needs hydration.
107
124
  */
108
- hasModulesInConfig(config, visited = /* @__PURE__ */ new Set()) {
109
- if (!config || visited.has(config)) {
110
- return false;
111
- }
112
- visited.add(config);
113
- if (config.dependencies?.modules?.some((entry) => entry.trim().length > 0)) {
114
- return true;
115
- }
116
- if (config.layout?.config && this.hasModulesInConfig(config.layout.config, visited)) {
117
- return true;
118
- }
119
- for (const component of config.dependencies?.components ?? []) {
120
- if (this.hasModulesInConfig(component.config, visited)) {
121
- return true;
122
- }
123
- }
124
- return false;
125
+ hasModulesInConfig(config) {
126
+ return someInConfigTree(
127
+ config,
128
+ (node) => node.dependencies?.modules?.some((entry) => entry.trim().length > 0) ?? false
129
+ );
125
130
  }
126
131
  /**
127
132
  * Determines whether a page needs client-side hydration.
@@ -150,6 +155,9 @@ class ReactPageModuleService {
150
155
  return Array.from(new Set(declarations));
151
156
  }
152
157
  }
158
+ function sanitizeCacheScope(cacheScope) {
159
+ return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
160
+ }
153
161
  export {
154
162
  ReactPageModuleService
155
163
  };
@@ -0,0 +1,46 @@
1
+ import type { HtmlTemplateProps, IntegrationRendererRenderOptions, RequestLocals } from '@ecopages/core';
2
+ import type { ReactNode } from 'react';
3
+ type PagePayloadOptions = {
4
+ pageProps?: HtmlTemplateProps['pageProps'];
5
+ params: IntegrationRendererRenderOptions<ReactNode>['params'];
6
+ query: IntegrationRendererRenderOptions<ReactNode>['query'];
7
+ safeLocals?: RequestLocals;
8
+ };
9
+ /**
10
+ * Builds the serialized page payload React exposes to document shells and the browser.
11
+ *
12
+ * This keeps hydration payload shaping away from the renderer so the renderer can
13
+ * stay focused on component orchestration instead of data serialization rules.
14
+ */
15
+ export declare class ReactPagePayloadService {
16
+ /**
17
+ * Creates the canonical page-props payload used by router hydration.
18
+ *
19
+ * React pages embedded in a non-React HTML shell still need to expose the same
20
+ * page-data contract as fully React-owned documents so navigation and hydration
21
+ * can read one shared document payload consistently.
22
+ */
23
+ buildRouterPageDataScript(pageProps: HtmlTemplateProps['pageProps'] | undefined): string;
24
+ /**
25
+ * Builds the serialized page-props payload embedded into the final HTML.
26
+ *
27
+ * The document payload is intentionally narrower than the full server render
28
+ * input: only routing data, public page props, and explicitly allowed locals are
29
+ * exposed to the browser.
30
+ */
31
+ buildSerializedPageProps(options: PagePayloadOptions): HtmlTemplateProps['pageProps'];
32
+ /**
33
+ * Safely extracts the declared subset of locals for client-side hydration.
34
+ *
35
+ * On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
36
+ * request-scoped data (e.g., session). Only keys explicitly declared via
37
+ * `Page.requires` are serialized to the client so sensitive request-only data
38
+ * is not leaked into hydration payloads by default.
39
+ *
40
+ * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
41
+ * to prevent accidental use. This method safely detects that case and returns
42
+ * `undefined` instead of throwing.
43
+ */
44
+ getSerializableLocals(locals: RequestLocals | undefined, requiredLocals?: string | readonly string[]): RequestLocals | undefined;
45
+ }
46
+ export {};