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