@ecopages/react 0.2.0-alpha.9 → 0.2.0-beta.0
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 +30 -13
- package/package.json +23 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +102 -18
- package/src/react-hmr-strategy.js +427 -50
- package/src/react-renderer.d.ts +100 -92
- package/src/react-renderer.js +356 -340
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +25 -107
- package/src/react.plugin.js +109 -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/pages-index.d.ts +64 -0
- package/src/services/pages-index.js +73 -0
- package/src/services/react-bundle.service.d.ts +24 -9
- package/src/services/react-bundle.service.js +35 -24
- package/src/services/react-hmr-page-metadata-cache.d.ts +10 -1
- 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 +83 -64
- 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 +8 -3
- package/src/services/react-page-module.service.js +33 -26
- 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 +9 -2
- package/src/services/react-runtime-bundle.service.js +77 -16
- package/src/utils/client-graph-boundary-cache.d.ts +108 -0
- package/src/utils/client-graph-boundary-cache.js +116 -0
- package/src/utils/client-graph-boundary-plugin.d.ts +13 -5
- package/src/utils/client-graph-boundary-plugin.js +63 -5
- 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 +9 -5
- package/src/utils/hydration-scripts.js +119 -34
- 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 +1 -1
- package/src/utils/react-dom-runtime-interop-plugin.js +9 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/{react-runtime-specifier-map.d.ts → react-runtime-alias-map.d.ts} +3 -1
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -27
- package/src/react-hmr-strategy.ts +0 -386
- package/src/react-renderer.ts +0 -803
- package/src/react.plugin.ts +0 -276
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -108
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -263
- package/src/services/react-page-module.service.ts +0 -224
- package/src/services/react-runtime-bundle.service.ts +0 -172
- package/src/utils/client-graph-boundary-plugin.ts +0 -831
- 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 -459
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-dom-runtime-interop-plugin.ts +0 -33
- package/src/utils/react-mdx-loader-plugin.ts +0 -63
- package/src/utils/react-runtime-specifier-map.js +0 -37
- package/src/utils/react-runtime-specifier-map.ts +0 -45
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +0 -5
- package/src/utils/use-sync-external-store-shim-plugin.js +0 -41
- package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
|
@@ -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,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
|
|
10
|
-
import type
|
|
10
|
+
import { type BuildExecutor } from '@ecopages/core/build/build-adapter';
|
|
11
11
|
import type { CompileOptions } from '@mdx-js/mdx';
|
|
12
12
|
/**
|
|
13
13
|
* Configuration for the ReactPageModuleService.
|
|
@@ -43,7 +43,12 @@ export declare class ReactPageModuleService {
|
|
|
43
43
|
* @param filePath - Absolute path to the MDX file
|
|
44
44
|
* @returns The imported module
|
|
45
45
|
*/
|
|
46
|
-
importMdxPageFile(filePath: string
|
|
46
|
+
importMdxPageFile(filePath: string, options?: {
|
|
47
|
+
bypassCache?: boolean;
|
|
48
|
+
cacheScope?: string;
|
|
49
|
+
}): Promise<EcoPageFile<{
|
|
50
|
+
config?: EcoComponentConfig;
|
|
51
|
+
}>>;
|
|
47
52
|
/**
|
|
48
53
|
* Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
|
|
49
54
|
* Resolves the file path from dependency declarations when not already set.
|
|
@@ -57,7 +62,7 @@ export declare class ReactPageModuleService {
|
|
|
57
62
|
* Recursively checks whether a component config tree declares any browser modules.
|
|
58
63
|
* Used to determine if a page needs hydration.
|
|
59
64
|
*/
|
|
60
|
-
hasModulesInConfig(config: EcoComponentConfig | undefined
|
|
65
|
+
hasModulesInConfig(config: EcoComponentConfig | undefined): boolean;
|
|
61
66
|
/**
|
|
62
67
|
* Determines whether a page needs client-side hydration.
|
|
63
68
|
*
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
|
-
import { rapidhash } from "@ecopages/core/hash";
|
|
4
3
|
import { build } from "@ecopages/core/build/build-adapter";
|
|
4
|
+
import { normalizeNodeRuntimeBuildOutputFile } from "@ecopages/core/build/runtime-build-output-normalizer";
|
|
5
|
+
import { rapidhash } from "@ecopages/core/hash";
|
|
5
6
|
import { fileSystem } from "@ecopages/file-system";
|
|
7
|
+
import { someInConfigTree } from "../utils/component-config-traversal.js";
|
|
6
8
|
import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
|
|
9
|
+
import { createReactMdxLoaderPlugin } from "../utils/react-mdx-loader-plugin.js";
|
|
7
10
|
class ReactPageModuleService {
|
|
8
11
|
config;
|
|
9
12
|
constructor(config) {
|
|
@@ -23,8 +26,7 @@ class ReactPageModuleService {
|
|
|
23
26
|
* @param filePath - Absolute path to the MDX file
|
|
24
27
|
* @returns The imported module
|
|
25
28
|
*/
|
|
26
|
-
async importMdxPageFile(filePath) {
|
|
27
|
-
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
29
|
+
async importMdxPageFile(filePath, options) {
|
|
28
30
|
const mdxPlugin = createReactMdxLoaderPlugin(
|
|
29
31
|
this.config.mdxCompilerOptions ?? {
|
|
30
32
|
jsxImportSource: "react",
|
|
@@ -35,20 +37,23 @@ class ReactPageModuleService {
|
|
|
35
37
|
const outdir = path.join(this.config.workDir, ".server-modules-react-mdx");
|
|
36
38
|
const fileBaseName = path.basename(filePath, path.extname(filePath));
|
|
37
39
|
const fileHash = fileSystem.hash(filePath);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
40
|
+
const cacheScopeSuffix = options?.cacheScope ? `-${sanitizeCacheScope(options.cacheScope)}` : "";
|
|
41
|
+
const cacheBuster = options?.bypassCache || process?.env?.NODE_ENV === "development" ? `-${Date.now()}` : "";
|
|
42
|
+
const outputFileName = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${cacheBuster}.mjs`;
|
|
43
|
+
const outputNamingTemplate = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${cacheBuster}.[ext]`;
|
|
40
44
|
const buildResult = await build(
|
|
41
45
|
{
|
|
42
46
|
entrypoints: [filePath],
|
|
43
47
|
root: this.config.rootDir,
|
|
44
48
|
outdir,
|
|
45
|
-
target: "
|
|
49
|
+
target: "es2022",
|
|
46
50
|
format: "esm",
|
|
47
51
|
sourcemap: "none",
|
|
48
52
|
splitting: false,
|
|
49
53
|
minify: false,
|
|
50
54
|
treeshaking: false,
|
|
51
|
-
|
|
55
|
+
externalPackages: true,
|
|
56
|
+
naming: outputNamingTemplate,
|
|
52
57
|
plugins: [mdxPlugin]
|
|
53
58
|
},
|
|
54
59
|
this.config.buildExecutor
|
|
@@ -58,11 +63,22 @@ class ReactPageModuleService {
|
|
|
58
63
|
throw new Error(`Failed to compile MDX page module: ${details}`);
|
|
59
64
|
}
|
|
60
65
|
const preferredOutputPath = path.join(outdir, outputFileName);
|
|
61
|
-
const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => output.path
|
|
66
|
+
const compiledOutput = buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ?? buildResult.outputs.find((output) => /\.(?:[cm]?js)$/u.test(output.path))?.path;
|
|
62
67
|
if (!compiledOutput) {
|
|
63
68
|
throw new Error(`No compiled MDX output generated for page: ${filePath}`);
|
|
64
69
|
}
|
|
65
|
-
|
|
70
|
+
normalizeNodeRuntimeBuildOutputFile(compiledOutput, this.config.rootDir);
|
|
71
|
+
const compiledOutputUrl = pathToFileURL(compiledOutput);
|
|
72
|
+
if (process?.env?.NODE_ENV === "development" || options?.cacheScope) {
|
|
73
|
+
compiledOutputUrl.searchParams.set(
|
|
74
|
+
"update",
|
|
75
|
+
[fileHash, options?.cacheScope ? sanitizeCacheScope(options.cacheScope) : void 0].filter((value) => value !== void 0).join("-")
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
return await import(
|
|
79
|
+
/* @vite-ignore */
|
|
80
|
+
compiledOutputUrl.href
|
|
81
|
+
);
|
|
66
82
|
}
|
|
67
83
|
/**
|
|
68
84
|
* Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
|
|
@@ -109,23 +125,11 @@ class ReactPageModuleService {
|
|
|
109
125
|
* Recursively checks whether a component config tree declares any browser modules.
|
|
110
126
|
* Used to determine if a page needs hydration.
|
|
111
127
|
*/
|
|
112
|
-
hasModulesInConfig(config
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
if (config.dependencies?.modules?.some((entry) => entry.trim().length > 0)) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
if (config.layout?.config && this.hasModulesInConfig(config.layout.config, visited)) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
124
|
-
if (this.hasModulesInConfig(component.config, visited)) {
|
|
125
|
-
return true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
return false;
|
|
128
|
+
hasModulesInConfig(config) {
|
|
129
|
+
return someInConfigTree(
|
|
130
|
+
config,
|
|
131
|
+
(node) => node.dependencies?.modules?.some((entry) => entry.trim().length > 0) ?? false
|
|
132
|
+
);
|
|
129
133
|
}
|
|
130
134
|
/**
|
|
131
135
|
* Determines whether a page needs client-side hydration.
|
|
@@ -154,6 +158,9 @@ class ReactPageModuleService {
|
|
|
154
158
|
return Array.from(new Set(declarations));
|
|
155
159
|
}
|
|
156
160
|
}
|
|
161
|
+
function sanitizeCacheScope(cacheScope) {
|
|
162
|
+
return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
163
|
+
}
|
|
157
164
|
export {
|
|
158
165
|
ReactPageModuleService
|
|
159
166
|
};
|
|
@@ -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 {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { LocalsAccessError } from "@ecopages/core/errors";
|
|
2
|
+
class ReactPagePayloadService {
|
|
3
|
+
/**
|
|
4
|
+
* Creates the canonical page-props payload used by router hydration.
|
|
5
|
+
*
|
|
6
|
+
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
7
|
+
* page-data contract as fully React-owned documents so navigation and hydration
|
|
8
|
+
* can read one shared document payload consistently.
|
|
9
|
+
*/
|
|
10
|
+
buildRouterPageDataScript(pageProps) {
|
|
11
|
+
const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
|
|
12
|
+
return `<script id="__ECO_PAGE_DATA__" type="application/json">${safeJson}<\/script>`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Builds the serialized page-props payload embedded into the final HTML.
|
|
16
|
+
*
|
|
17
|
+
* The document payload is intentionally narrower than the full server render
|
|
18
|
+
* input: only routing data, public page props, and explicitly allowed locals are
|
|
19
|
+
* exposed to the browser.
|
|
20
|
+
*/
|
|
21
|
+
buildSerializedPageProps(options) {
|
|
22
|
+
return {
|
|
23
|
+
...options.pageProps,
|
|
24
|
+
params: options.params,
|
|
25
|
+
query: options.query,
|
|
26
|
+
...options.safeLocals && { locals: options.safeLocals }
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Safely extracts the declared subset of locals for client-side hydration.
|
|
31
|
+
*
|
|
32
|
+
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
33
|
+
* request-scoped data (e.g., session). Only keys explicitly declared via
|
|
34
|
+
* `Page.requires` are serialized to the client so sensitive request-only data
|
|
35
|
+
* is not leaked into hydration payloads by default.
|
|
36
|
+
*
|
|
37
|
+
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
38
|
+
* to prevent accidental use. This method safely detects that case and returns
|
|
39
|
+
* `undefined` instead of throwing.
|
|
40
|
+
*/
|
|
41
|
+
getSerializableLocals(locals, requiredLocals) {
|
|
42
|
+
try {
|
|
43
|
+
if (!locals) {
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
const requiredKeys = requiredLocals ? Array.isArray(requiredLocals) ? requiredLocals : [requiredLocals] : [];
|
|
47
|
+
if (requiredKeys.length === 0) {
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
const serializedLocals = Object.fromEntries(
|
|
51
|
+
requiredKeys.filter((key) => Object.prototype.hasOwnProperty.call(locals, key)).map((key) => [key, locals[key]])
|
|
52
|
+
);
|
|
53
|
+
if (Object.keys(serializedLocals).length > 0) {
|
|
54
|
+
return serializedLocals;
|
|
55
|
+
}
|
|
56
|
+
return void 0;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (error instanceof LocalsAccessError) {
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export {
|
|
66
|
+
ReactPagePayloadService
|
|
67
|
+
};
|
|
@@ -6,32 +6,39 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
|
-
import type { EcoBuildPlugin } from '@ecopages/core/
|
|
9
|
+
import type { EcoBuildPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
10
10
|
import { type AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
11
11
|
import type { ReactRouterAdapter } from '../router-adapter.js';
|
|
12
|
+
import { type BrowserRuntimeManifest } from '@ecopages/core/build/browser-runtime-manifest';
|
|
12
13
|
export type ReactRuntimeImports = {
|
|
13
14
|
react: string;
|
|
14
15
|
reactDomClient: string;
|
|
15
16
|
reactJsxRuntime: string;
|
|
16
17
|
reactJsxDevRuntime: string;
|
|
17
18
|
reactDom: string;
|
|
19
|
+
useSyncExternalStoreWithSelector: string;
|
|
18
20
|
router?: string;
|
|
19
21
|
};
|
|
20
22
|
export interface ReactRuntimeBundleServiceConfig {
|
|
21
23
|
routerAdapter?: ReactRouterAdapter;
|
|
24
|
+
rootDir?: string;
|
|
22
25
|
}
|
|
23
26
|
type RuntimeMode = 'development' | 'production';
|
|
24
27
|
export declare class ReactRuntimeBundleService {
|
|
25
28
|
private readonly config;
|
|
26
29
|
constructor(config: ReactRuntimeBundleServiceConfig);
|
|
30
|
+
setRootDir(rootDir: string | undefined): void;
|
|
27
31
|
private get isDevelopment();
|
|
28
32
|
private getCurrentRuntimeMode;
|
|
29
33
|
private createRuntimeDefines;
|
|
30
34
|
private getReactVendorFileName;
|
|
31
35
|
private getReactDomVendorFileName;
|
|
32
36
|
private getRouterVendorFileName;
|
|
37
|
+
private getUseSyncExternalStoreWithSelectorVendorFileName;
|
|
38
|
+
private createReactVendorImportRewritePlugin;
|
|
33
39
|
getRuntimeImports(mode?: RuntimeMode): ReactRuntimeImports;
|
|
34
|
-
|
|
40
|
+
getRuntimeAliasMap(mode?: RuntimeMode): Record<string, string>;
|
|
41
|
+
getRuntimeManifest(mode?: RuntimeMode): BrowserRuntimeManifest;
|
|
35
42
|
getDependencies(): AssetDefinition[];
|
|
36
43
|
createRuntimeAliasPlugin(mode?: RuntimeMode): EcoBuildPlugin;
|
|
37
44
|
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createBrowserRuntimePlugin } from "@ecopages/core/build/browser-runtime-plugin";
|
|
2
|
+
import { DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME } from "@ecopages/core/build/browser-runtime-plugin";
|
|
2
3
|
import {
|
|
3
4
|
buildBrowserRuntimeAssetUrl,
|
|
4
5
|
createBrowserRuntimeModuleAsset,
|
|
5
6
|
createBrowserRuntimeScriptAsset
|
|
6
7
|
} from "@ecopages/core/services/asset-processing-service";
|
|
7
8
|
import { createReactDomRuntimeInteropPlugin } from "../utils/react-dom-runtime-interop-plugin.js";
|
|
8
|
-
import {
|
|
9
|
+
import { buildReactRuntimeAliasMap, buildReactRuntimeManifest } from "../utils/react-runtime-alias-map.js";
|
|
10
|
+
import {
|
|
11
|
+
createBrowserRuntimeManifest
|
|
12
|
+
} from "@ecopages/core/build/browser-runtime-manifest";
|
|
9
13
|
class ReactRuntimeBundleService {
|
|
10
14
|
config;
|
|
11
15
|
constructor(config) {
|
|
12
16
|
this.config = config;
|
|
13
17
|
}
|
|
18
|
+
setRootDir(rootDir) {
|
|
19
|
+
this.config.rootDir = rootDir;
|
|
20
|
+
}
|
|
14
21
|
get isDevelopment() {
|
|
15
22
|
return process.env.NODE_ENV === "development";
|
|
16
23
|
}
|
|
@@ -36,6 +43,22 @@ class ReactRuntimeBundleService {
|
|
|
36
43
|
}
|
|
37
44
|
return mode === "development" ? `${this.config.routerAdapter.bundle.outputName}.development.js` : `${this.config.routerAdapter.bundle.outputName}.js`;
|
|
38
45
|
}
|
|
46
|
+
getUseSyncExternalStoreWithSelectorVendorFileName(mode) {
|
|
47
|
+
return mode === "development" ? "use-sync-external-store-with-selector.development.js" : "use-sync-external-store-with-selector.js";
|
|
48
|
+
}
|
|
49
|
+
createReactVendorImportRewritePlugin(mode) {
|
|
50
|
+
return createBrowserRuntimePlugin({
|
|
51
|
+
name: `react-plugin-vendor-runtime-import-rewrite-${mode}`,
|
|
52
|
+
manifest: createBrowserRuntimeManifest([
|
|
53
|
+
{
|
|
54
|
+
specifier: "react",
|
|
55
|
+
owner: "@ecopages/react",
|
|
56
|
+
importPath: "react",
|
|
57
|
+
publicPath: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
|
|
58
|
+
}
|
|
59
|
+
])
|
|
60
|
+
});
|
|
61
|
+
}
|
|
39
62
|
getRuntimeImports(mode = this.getCurrentRuntimeMode()) {
|
|
40
63
|
const reactVendorFileName = this.getReactVendorFileName(mode);
|
|
41
64
|
const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
|
|
@@ -44,31 +67,45 @@ class ReactRuntimeBundleService {
|
|
|
44
67
|
reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
45
68
|
reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
46
69
|
reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
47
|
-
reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName)
|
|
70
|
+
reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
71
|
+
useSyncExternalStoreWithSelector: buildBrowserRuntimeAssetUrl(
|
|
72
|
+
this.getUseSyncExternalStoreWithSelectorVendorFileName(mode)
|
|
73
|
+
)
|
|
48
74
|
};
|
|
49
75
|
if (this.config.routerAdapter) {
|
|
50
76
|
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
51
77
|
}
|
|
52
78
|
return runtimeImports;
|
|
53
79
|
}
|
|
54
|
-
|
|
55
|
-
return
|
|
80
|
+
getRuntimeAliasMap(mode = this.getCurrentRuntimeMode()) {
|
|
81
|
+
return buildReactRuntimeAliasMap(this.getRuntimeImports(mode));
|
|
82
|
+
}
|
|
83
|
+
getRuntimeManifest(mode = this.getCurrentRuntimeMode()) {
|
|
84
|
+
return buildReactRuntimeManifest(this.getRuntimeImports(mode));
|
|
56
85
|
}
|
|
57
86
|
getDependencies() {
|
|
58
|
-
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
|
|
59
87
|
const dependencies = [];
|
|
60
88
|
for (const mode of ["production", "development"]) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
89
|
+
const reactVendorImportRewritePlugin = this.createReactVendorImportRewritePlugin(mode);
|
|
90
|
+
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin({
|
|
91
|
+
reactSpecifier: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
|
|
92
|
+
});
|
|
93
|
+
const reactRuntimeAliasPlugin = createBrowserRuntimePlugin({
|
|
94
|
+
name: `react-plugin-runtime-specifier-alias-${mode}`,
|
|
95
|
+
manifest: createBrowserRuntimeManifest([
|
|
96
|
+
{
|
|
97
|
+
specifier: "react",
|
|
98
|
+
owner: "",
|
|
99
|
+
importPath: "react",
|
|
100
|
+
publicPath: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode))
|
|
101
|
+
}
|
|
102
|
+
])
|
|
103
|
+
});
|
|
67
104
|
const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
|
|
68
105
|
(plugin) => plugin !== null
|
|
69
106
|
);
|
|
70
107
|
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
|
|
71
|
-
const mappedSpecifiers = new Set(Object.keys(this.
|
|
108
|
+
const mappedSpecifiers = new Set(Object.keys(this.getRuntimeAliasMap(mode)));
|
|
72
109
|
dependencies.push(
|
|
73
110
|
createBrowserRuntimeModuleAsset({
|
|
74
111
|
modules: [
|
|
@@ -79,8 +116,10 @@ class ReactRuntimeBundleService {
|
|
|
79
116
|
name: "react",
|
|
80
117
|
fileName: this.getReactVendorFileName(mode),
|
|
81
118
|
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
119
|
+
rootDir: this.config.rootDir,
|
|
82
120
|
bundleOptions: {
|
|
83
|
-
define: this.createRuntimeDefines(mode)
|
|
121
|
+
define: this.createRuntimeDefines(mode),
|
|
122
|
+
excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME]
|
|
84
123
|
}
|
|
85
124
|
}),
|
|
86
125
|
createBrowserRuntimeModuleAsset({
|
|
@@ -88,10 +127,22 @@ class ReactRuntimeBundleService {
|
|
|
88
127
|
name: "react-dom",
|
|
89
128
|
fileName: this.getReactDomVendorFileName(mode),
|
|
90
129
|
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
130
|
+
rootDir: this.config.rootDir,
|
|
91
131
|
bundleOptions: {
|
|
92
132
|
define: this.createRuntimeDefines(mode),
|
|
133
|
+
excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME],
|
|
93
134
|
plugins: reactDomBundlePlugins
|
|
94
135
|
}
|
|
136
|
+
}),
|
|
137
|
+
createBrowserRuntimeScriptAsset({
|
|
138
|
+
importPath: "@ecopages/react/runtime/use-sync-external-store-with-selector",
|
|
139
|
+
name: "use-sync-external-store-with-selector",
|
|
140
|
+
fileName: this.getUseSyncExternalStoreWithSelectorVendorFileName(mode),
|
|
141
|
+
bundleOptions: {
|
|
142
|
+
define: this.createRuntimeDefines(mode),
|
|
143
|
+
excludeAppBuildPlugins: [DEFAULT_BROWSER_RUNTIME_PLUGIN_NAME],
|
|
144
|
+
plugins: [reactVendorImportRewritePlugin]
|
|
145
|
+
}
|
|
95
146
|
})
|
|
96
147
|
);
|
|
97
148
|
if (this.config.routerAdapter) {
|
|
@@ -115,8 +166,18 @@ class ReactRuntimeBundleService {
|
|
|
115
166
|
return dependencies;
|
|
116
167
|
}
|
|
117
168
|
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()) {
|
|
118
|
-
|
|
119
|
-
|
|
169
|
+
const aliasMap = this.getRuntimeAliasMap(mode);
|
|
170
|
+
const manifest = createBrowserRuntimeManifest(
|
|
171
|
+
Object.entries(aliasMap).map(([specifier, publicPath]) => ({
|
|
172
|
+
specifier,
|
|
173
|
+
owner: "",
|
|
174
|
+
importPath: specifier,
|
|
175
|
+
publicPath
|
|
176
|
+
}))
|
|
177
|
+
);
|
|
178
|
+
return createBrowserRuntimePlugin({
|
|
179
|
+
name: `react-plugin-runtime-alias-${mode}`,
|
|
180
|
+
manifest
|
|
120
181
|
});
|
|
121
182
|
}
|
|
122
183
|
}
|