@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.50
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 +16 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +60 -43
- package/src/react-hmr-strategy.js +297 -144
- 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 +38 -111
- package/src/react.plugin.js +132 -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/services/react-bundle.service.d.ts +19 -31
- package/src/services/react-bundle.service.js +51 -100
- 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 +15 -13
- package/src/services/react-runtime-bundle.service.js +103 -180
- 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 +29 -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 +6 -0
- package/src/utils/react-runtime-alias-map.js +33 -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/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
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { createClientGraphBoundaryPlugin } from "../utils/client-graph-boundary-plugin.js";
|
|
2
|
+
import {
|
|
3
|
+
buildReactRuntimeAliasMap,
|
|
4
|
+
getReactClientGraphAllowSpecifiers,
|
|
5
|
+
getReactRuntimeExternalSpecifiers
|
|
6
|
+
} from "../utils/react-runtime-alias-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";
|
|
11
|
+
import { createReactMdxLoaderPlugin } from "../utils/react-mdx-loader-plugin.js";
|
|
3
12
|
class ReactBundleService {
|
|
13
|
+
runtimeBundleService;
|
|
14
|
+
config;
|
|
4
15
|
constructor(config) {
|
|
5
16
|
this.config = config;
|
|
6
17
|
this.runtimeBundleService = new ReactRuntimeBundleService({
|
|
18
|
+
rootDir: config.rootDir,
|
|
7
19
|
routerAdapter: config.routerAdapter
|
|
8
20
|
});
|
|
9
21
|
}
|
|
10
|
-
runtimeBundleService;
|
|
11
22
|
/**
|
|
12
23
|
* Returns resolved runtime import paths for the React runtime.
|
|
13
24
|
*/
|
|
@@ -22,39 +33,58 @@ class ReactBundleService {
|
|
|
22
33
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
23
34
|
* @returns Bundle options object for the build adapter
|
|
24
35
|
*/
|
|
25
|
-
async createBundleOptions(componentName, isMdx, declaredModules) {
|
|
36
|
+
async createBundleOptions(componentName, isMdx, declaredModules, bundleOptions = {}) {
|
|
26
37
|
const runtimeImports = this.getRuntimeImports();
|
|
27
38
|
const options = {
|
|
28
|
-
external: ["react", "react-dom", "react/jsx-runtime", "react/jsx-dev-runtime", "react-dom/client"],
|
|
29
39
|
mainFields: ["module", "browser", "main"],
|
|
30
40
|
naming: `${componentName}.[ext]`,
|
|
31
41
|
...import.meta.env?.NODE_ENV === "production" && {
|
|
32
42
|
minify: true,
|
|
33
|
-
splitting: false,
|
|
34
43
|
treeshaking: true
|
|
35
|
-
}
|
|
44
|
+
},
|
|
45
|
+
...bundleOptions.splitting === void 0 ? {} : { splitting: bundleOptions.splitting }
|
|
36
46
|
};
|
|
47
|
+
if (!bundleOptions.includeRuntime) {
|
|
48
|
+
const reactRuntimeSpecifiers = new Set(getReactRuntimeExternalSpecifiers());
|
|
49
|
+
options.external = [
|
|
50
|
+
...Object.values(runtimeImports).filter(
|
|
51
|
+
(specifier) => Boolean(specifier) && !reactRuntimeSpecifiers.has(
|
|
52
|
+
specifier
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
];
|
|
56
|
+
}
|
|
37
57
|
const graphBoundaryPlugin = createClientGraphBoundaryPlugin({
|
|
38
58
|
absWorkingDir: this.config.rootDir,
|
|
39
59
|
declaredModules,
|
|
40
|
-
alwaysAllowSpecifiers: [
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"react-dom/client",
|
|
47
|
-
...this.config.routerAdapter ? [this.config.routerAdapter.importMapKey] : []
|
|
48
|
-
]
|
|
60
|
+
alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
|
|
61
|
+
});
|
|
62
|
+
const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
|
|
63
|
+
name: "react-renderer-foreign-jsx-override",
|
|
64
|
+
hostJsxImportSource: this.config.jsxImportSource ?? "react",
|
|
65
|
+
foreignExtensions: this.config.nonReactExtensions ?? []
|
|
49
66
|
});
|
|
50
|
-
const
|
|
51
|
-
|
|
67
|
+
const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
|
|
68
|
+
name: "react-renderer-use-sync-external-store-shim",
|
|
69
|
+
namespace: "ecopages-react-renderer-shim"
|
|
70
|
+
});
|
|
71
|
+
const runtimePlugins = bundleOptions.includeRuntime ? [] : [this.createRuntimeAliasPlugin(buildReactRuntimeAliasMap(runtimeImports))];
|
|
52
72
|
if (isMdx && this.config.mdxCompilerOptions) {
|
|
53
|
-
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
54
73
|
const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
|
|
55
|
-
options.plugins = [
|
|
74
|
+
options.plugins = [
|
|
75
|
+
foreignJsxOverridePlugin,
|
|
76
|
+
graphBoundaryPlugin,
|
|
77
|
+
...runtimePlugins,
|
|
78
|
+
mdxPlugin,
|
|
79
|
+
useSyncExternalStoreShimPlugin
|
|
80
|
+
];
|
|
56
81
|
} else {
|
|
57
|
-
options.plugins = [
|
|
82
|
+
options.plugins = [
|
|
83
|
+
foreignJsxOverridePlugin,
|
|
84
|
+
graphBoundaryPlugin,
|
|
85
|
+
...runtimePlugins,
|
|
86
|
+
useSyncExternalStoreShimPlugin
|
|
87
|
+
];
|
|
58
88
|
}
|
|
59
89
|
return options;
|
|
60
90
|
}
|
|
@@ -62,87 +92,8 @@ class ReactBundleService {
|
|
|
62
92
|
* Creates the esbuild plugin that rewrites bare React specifiers
|
|
63
93
|
* to their runtime asset URLs.
|
|
64
94
|
*/
|
|
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
|
-
* Redirects `use-sync-external-store/shim` imports to React's built-in
|
|
98
|
-
* `useSyncExternalStore`.
|
|
99
|
-
*
|
|
100
|
-
* Libraries like React Aria still list `use-sync-external-store` as a
|
|
101
|
-
* dependency to support React 16/17. On React 18+ the `/shim` export is
|
|
102
|
-
* already a pass-through, but without this plugin esbuild would bundle
|
|
103
|
-
* the full CJS shim (including `process.env` branching) into the browser
|
|
104
|
-
* bundle. The plugin short-circuits the resolution so only a single clean
|
|
105
|
-
* ESM re-export is emitted.
|
|
106
|
-
*/
|
|
107
|
-
createSyncExternalStorePlugin() {
|
|
108
|
-
return {
|
|
109
|
-
name: "react-renderer-use-sync-external-store-shim",
|
|
110
|
-
setup(build) {
|
|
111
|
-
build.onResolve({ filter: /^use-sync-external-store\/shim(?:\/index\.js)?$/ }, () => ({
|
|
112
|
-
path: "use-sync-external-store/shim",
|
|
113
|
-
namespace: "ecopages-react-renderer-shim"
|
|
114
|
-
}));
|
|
115
|
-
build.onLoad(
|
|
116
|
-
{ filter: /^use-sync-external-store\/shim$/, namespace: "ecopages-react-renderer-shim" },
|
|
117
|
-
() => ({
|
|
118
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
119
|
-
loader: "js"
|
|
120
|
-
})
|
|
121
|
-
);
|
|
122
|
-
build.onLoad({ filter: /[\\/]use-sync-external-store[\\/]shim[\\/]index\.js$/ }, () => ({
|
|
123
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
124
|
-
loader: "js"
|
|
125
|
-
}));
|
|
126
|
-
build.onLoad(
|
|
127
|
-
{
|
|
128
|
-
filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.development\.js$/
|
|
129
|
-
},
|
|
130
|
-
() => ({
|
|
131
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
132
|
-
loader: "js"
|
|
133
|
-
})
|
|
134
|
-
);
|
|
135
|
-
build.onLoad(
|
|
136
|
-
{
|
|
137
|
-
filter: /[\\/]use-sync-external-store[\\/]cjs[\\/]use-sync-external-store-shim\.production\.js$/
|
|
138
|
-
},
|
|
139
|
-
() => ({
|
|
140
|
-
contents: "export { useSyncExternalStore } from 'react';",
|
|
141
|
-
loader: "js"
|
|
142
|
-
})
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
};
|
|
95
|
+
createRuntimeAliasPlugin(runtimeAliasMap) {
|
|
96
|
+
return createRuntimeSpecifierAliasPlugin(runtimeAliasMap, { name: "react-runtime-import-alias" });
|
|
146
97
|
}
|
|
147
98
|
}
|
|
148
99
|
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
|
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hydration asset creation service for React integration.
|
|
3
3
|
*
|
|
4
|
-
* Builds the asset definitions
|
|
5
|
-
*
|
|
6
|
-
* island level.
|
|
4
|
+
* Builds the asset definitions required for client-side React rendering — both at
|
|
5
|
+
* the page level and the component island level.
|
|
7
6
|
*
|
|
8
7
|
* @module
|
|
9
8
|
*/
|
|
@@ -23,53 +22,63 @@ export interface ReactHydrationAssetServiceConfig {
|
|
|
23
22
|
bundleService: ReactBundleService;
|
|
24
23
|
hmrPageMetadataCache?: ReactHmrPageMetadataCache;
|
|
25
24
|
}
|
|
25
|
+
export declare function getReactIslandComponentKey(componentFile: string, config?: EcoComponentConfig): string;
|
|
26
26
|
/**
|
|
27
27
|
* Manages the creation of client-side hydration assets for React pages and component islands.
|
|
28
28
|
*/
|
|
29
29
|
export declare class ReactHydrationAssetService {
|
|
30
30
|
private readonly config;
|
|
31
|
+
private static readonly ROUTER_PAGE_GROUPED_BUNDLE_ID;
|
|
31
32
|
constructor(config: ReactHydrationAssetServiceConfig);
|
|
33
|
+
private getIslandBundleName;
|
|
34
|
+
private getIslandHydrationName;
|
|
35
|
+
private getRouterPageGroupedEntryName;
|
|
32
36
|
/**
|
|
33
|
-
* Resolves the import path for
|
|
37
|
+
* Resolves the browser import path used for a React-owned page or island module.
|
|
34
38
|
* Uses HMR manager for development or constructs static path for production.
|
|
35
39
|
*
|
|
36
40
|
* @param pagePath - Absolute path to the page source file
|
|
37
|
-
* @param
|
|
38
|
-
* @returns The resolved import path for the
|
|
41
|
+
* @param assetName - Generated asset name
|
|
42
|
+
* @returns The resolved browser import path for the module
|
|
39
43
|
*/
|
|
40
|
-
resolveAssetImportPath(pagePath: string,
|
|
44
|
+
resolveAssetImportPath(pagePath: string, assetName: string): Promise<string>;
|
|
41
45
|
/**
|
|
42
|
-
* Creates the
|
|
46
|
+
* Creates the page-owned route entry asset for hydration and client navigation.
|
|
43
47
|
*
|
|
44
48
|
* @param pagePath - Absolute path to the page source file
|
|
45
49
|
* @param componentName - Generated unique component name
|
|
46
|
-
* @param importPath - Resolved import path
|
|
50
|
+
* @param importPath - Resolved browser import path used by development HMR
|
|
47
51
|
* @param bundleOptions - Bundle configuration options
|
|
48
52
|
* @param isDevelopment - Whether running in development mode with HMR
|
|
49
53
|
* @param isMdx - Whether the source file is an MDX file
|
|
50
|
-
* @
|
|
51
|
-
* @returns Array of asset definitions for processing
|
|
54
|
+
* @returns One page-owned asset definition for processing
|
|
52
55
|
*/
|
|
53
|
-
createPageDependencies(pagePath: string, componentName: string, importPath: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean,
|
|
56
|
+
createPageDependencies(pagePath: string, componentName: string, importPath: string, pageModuleUrlExpression: string, bundleOptions: Record<string, unknown>, isDevelopment: boolean, useBrowserRuntimeImports: boolean, isMdx: boolean): AssetDefinition[];
|
|
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
|
+
* Creates the Page Browser Graph dependency declarations for a React page.
|
|
68
69
|
*
|
|
69
70
|
* @param pagePath - Absolute file path of the page
|
|
70
71
|
* @param isMdx - Whether the page is an MDX file
|
|
71
72
|
* @param declaredModules - Explicitly declared browser module specifiers
|
|
72
|
-
* @returns
|
|
73
|
+
* @returns Declarative assets for core-owned processing
|
|
74
|
+
*/
|
|
75
|
+
createPageBrowserGraphDependencies(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<AssetDefinition[]>;
|
|
76
|
+
/**
|
|
77
|
+
* Builds the Page Browser Graph assets for a React page.
|
|
78
|
+
*
|
|
79
|
+
* @remarks
|
|
80
|
+
* Kept as a compatibility wrapper while callers migrate to core-owned page
|
|
81
|
+
* graph assembly.
|
|
73
82
|
*/
|
|
74
|
-
|
|
83
|
+
buildPageBrowserGraphAssets(pagePath: string, isMdx: boolean, declaredModules: string[]): Promise<ProcessedAsset[]>;
|
|
75
84
|
}
|
|
@@ -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 {};
|