@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.
- package/README.md +152 -29
- package/package.json +24 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +65 -43
- package/src/react-hmr-strategy.js +298 -145
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +40 -111
- package/src/react.plugin.js +136 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/react-bundle.service.d.ts +22 -35
- package/src/services/react-bundle.service.js +41 -105
- package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
- package/src/services/react-hmr-page-metadata-cache.js +18 -2
- package/src/services/react-hydration-asset.service.d.ts +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
- package/src/services/react-mdx-config-dependency.service.js +122 -0
- package/src/services/react-page-module.service.d.ts +10 -2
- package/src/services/react-page-module.service.js +47 -39
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +20 -13
- package/src/services/react-runtime-bundle.service.js +146 -179
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +27 -6
- package/src/utils/hydration-scripts.js +177 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +8 -0
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -217
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -710
- package/src/utils/client-only.ts +0 -27
- package/src/utils/declared-modules.ts +0 -99
- package/src/utils/dynamic.ts +0 -27
- package/src/utils/hmr-scripts.ts +0 -47
- package/src/utils/html-boundary.ts +0 -66
- package/src/utils/hydration-scripts.ts +0 -338
- package/src/utils/reachability-analyzer.ts +0 -593
- 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
|
|
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
|
|
20
|
-
* @returns The resolved import path for the
|
|
33
|
+
* @param assetName - Generated asset name
|
|
34
|
+
* @returns The resolved browser import path for the module
|
|
21
35
|
*/
|
|
22
|
-
async resolveAssetImportPath(pagePath,
|
|
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), `${
|
|
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
|
|
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
|
|
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
|
-
* @
|
|
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,
|
|
54
|
+
createPageDependencies(pagePath, componentName, importPath, pageModuleUrlExpression, bundleOptions, isDevelopment, useBrowserRuntimeImports, isMdx) {
|
|
42
55
|
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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:
|
|
84
|
-
|
|
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":
|
|
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
|
|
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,
|
|
108
|
-
const componentName =
|
|
109
|
-
const
|
|
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-
|
|
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:
|
|
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":
|
|
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
|
-
*
|
|
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
|
|
166
|
+
* @returns Declarative assets for core-owned processing
|
|
168
167
|
*/
|
|
169
|
-
async
|
|
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
|
|
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
|
|
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 {
|
|
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.
|
|
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
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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 {};
|