@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
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hydration asset creation service for React integration.
|
|
3
|
-
*
|
|
4
|
-
* Builds the asset definitions (bundled component scripts + hydration bootstrap scripts)
|
|
5
|
-
* required for client-side React rendering — both at the page level and the component
|
|
6
|
-
* island level.
|
|
7
|
-
*
|
|
8
|
-
* @module
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import type { EcoComponentConfig } from '@ecopages/core';
|
|
13
|
-
import { rapidhash } from '@ecopages/core/hash';
|
|
14
|
-
import { RESOLVED_ASSETS_DIR } from '@ecopages/core/constants';
|
|
15
|
-
import {
|
|
16
|
-
AssetFactory,
|
|
17
|
-
type AssetDefinition,
|
|
18
|
-
type ProcessedAsset,
|
|
19
|
-
} from '@ecopages/core/services/asset-processing-service';
|
|
20
|
-
import type { AssetProcessingService } from '@ecopages/core/services/asset-processing-service';
|
|
21
|
-
import { createHydrationScript, createIslandHydrationScript } from '../utils/hydration-scripts.ts';
|
|
22
|
-
import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
|
|
23
|
-
import type { ReactBundleService } from './react-bundle.service.ts';
|
|
24
|
-
import type { ReactHmrPageMetadataCache } from './react-hmr-page-metadata-cache.ts';
|
|
25
|
-
import type { ReactRouterAdapter } from '../router-adapter.ts';
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Configuration for the ReactHydrationAssetService.
|
|
29
|
-
*/
|
|
30
|
-
export interface ReactHydrationAssetServiceConfig {
|
|
31
|
-
srcDir: string;
|
|
32
|
-
routerAdapter?: ReactRouterAdapter;
|
|
33
|
-
assetProcessingService: AssetProcessingService;
|
|
34
|
-
bundleService: ReactBundleService;
|
|
35
|
-
hmrPageMetadataCache?: ReactHmrPageMetadataCache;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Manages the creation of client-side hydration assets for React pages and component islands.
|
|
40
|
-
*/
|
|
41
|
-
export class ReactHydrationAssetService {
|
|
42
|
-
private readonly config: ReactHydrationAssetServiceConfig;
|
|
43
|
-
|
|
44
|
-
constructor(config: ReactHydrationAssetServiceConfig) {
|
|
45
|
-
this.config = config;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Resolves the import path for the bundled page component.
|
|
50
|
-
* Uses HMR manager for development or constructs static path for production.
|
|
51
|
-
*
|
|
52
|
-
* @param pagePath - Absolute path to the page source file
|
|
53
|
-
* @param componentName - Generated unique component name
|
|
54
|
-
* @returns The resolved import path for the bundled component
|
|
55
|
-
*/
|
|
56
|
-
async resolveAssetImportPath(pagePath: string, componentName: string): Promise<string> {
|
|
57
|
-
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
58
|
-
|
|
59
|
-
if (hmrManager?.isEnabled()) {
|
|
60
|
-
return hmrManager.registerEntrypoint(pagePath);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return `/${path
|
|
64
|
-
.join(RESOLVED_ASSETS_DIR, path.relative(this.config.srcDir, pagePath))
|
|
65
|
-
.replace(path.basename(pagePath), `${componentName}.js`)
|
|
66
|
-
.replace(/\\/g, '/')}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Creates the asset dependencies for a page: the bundled component and hydration script.
|
|
71
|
-
*
|
|
72
|
-
* @param pagePath - Absolute path to the page source file
|
|
73
|
-
* @param componentName - Generated unique component name
|
|
74
|
-
* @param importPath - Resolved import path for the bundled component
|
|
75
|
-
* @param bundleOptions - Bundle configuration options
|
|
76
|
-
* @param isDevelopment - Whether running in development mode with HMR
|
|
77
|
-
* @param isMdx - Whether the source file is an MDX file
|
|
78
|
-
* @param props - Optional page props for client serialization
|
|
79
|
-
* @returns Array of asset definitions for processing
|
|
80
|
-
*/
|
|
81
|
-
createPageDependencies(
|
|
82
|
-
pagePath: string,
|
|
83
|
-
componentName: string,
|
|
84
|
-
importPath: string,
|
|
85
|
-
bundleOptions: Record<string, unknown>,
|
|
86
|
-
isDevelopment: boolean,
|
|
87
|
-
isMdx: boolean,
|
|
88
|
-
props?: Record<string, unknown>,
|
|
89
|
-
): AssetDefinition[] {
|
|
90
|
-
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
91
|
-
const dependencies: AssetDefinition[] = [
|
|
92
|
-
AssetFactory.createFileScript({
|
|
93
|
-
position: 'head',
|
|
94
|
-
filepath: pagePath,
|
|
95
|
-
name: componentName,
|
|
96
|
-
excludeFromHtml: true,
|
|
97
|
-
bundle: true,
|
|
98
|
-
bundleOptions,
|
|
99
|
-
attributes: {
|
|
100
|
-
type: 'module',
|
|
101
|
-
defer: '',
|
|
102
|
-
'data-eco-persist': 'true',
|
|
103
|
-
},
|
|
104
|
-
}),
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
if (props && Object.keys(props).length > 0) {
|
|
108
|
-
dependencies.push(
|
|
109
|
-
AssetFactory.createContentScript({
|
|
110
|
-
position: 'head',
|
|
111
|
-
content: `window.__ECO_PAGES__=window.__ECO_PAGES__||{};window.__ECO_PAGES__.page={module:"${importPath}",props:${JSON.stringify(props)}};`,
|
|
112
|
-
name: `${componentName}-props`,
|
|
113
|
-
bundle: false,
|
|
114
|
-
attributes: {
|
|
115
|
-
type: 'module',
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
dependencies.push(
|
|
122
|
-
AssetFactory.createContentScript({
|
|
123
|
-
position: 'head',
|
|
124
|
-
content: createHydrationScript({
|
|
125
|
-
importPath,
|
|
126
|
-
reactImportPath: runtimeImports.react,
|
|
127
|
-
reactDomClientImportPath: runtimeImports.reactDomClient,
|
|
128
|
-
routerImportPath: runtimeImports.router,
|
|
129
|
-
isDevelopment,
|
|
130
|
-
isMdx,
|
|
131
|
-
router: this.config.routerAdapter,
|
|
132
|
-
}),
|
|
133
|
-
name: `${componentName}-hydration`,
|
|
134
|
-
bundle: false,
|
|
135
|
-
attributes: {
|
|
136
|
-
type: 'module',
|
|
137
|
-
defer: '',
|
|
138
|
-
'data-eco-rerun': 'true',
|
|
139
|
-
'data-eco-script-id': `${componentName}-hydration`,
|
|
140
|
-
'data-eco-persist': 'true',
|
|
141
|
-
},
|
|
142
|
-
}),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
return dependencies;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Builds client-side assets for a React component island.
|
|
150
|
-
*
|
|
151
|
-
* Includes the bundled component entry and an inline hydration bootstrap script.
|
|
152
|
-
*
|
|
153
|
-
* @param componentFile - Absolute path to the component source file
|
|
154
|
-
* @param componentInstanceId - Unique instance ID for DOM targeting
|
|
155
|
-
* @param props - Serialized props for client-side hydration
|
|
156
|
-
* @param config - Optional component config with `__eco` metadata
|
|
157
|
-
* @returns Processed assets ready for injection
|
|
158
|
-
*/
|
|
159
|
-
async buildComponentRenderAssets(
|
|
160
|
-
componentFile: string,
|
|
161
|
-
componentInstanceId: string,
|
|
162
|
-
props: Record<string, unknown>,
|
|
163
|
-
config?: EcoComponentConfig,
|
|
164
|
-
): Promise<ProcessedAsset[]> {
|
|
165
|
-
const componentName = `ecopages-react-island-${rapidhash(`${componentFile}:${componentInstanceId}`)}`;
|
|
166
|
-
const importPath = await this.resolveAssetImportPath(componentFile, componentName);
|
|
167
|
-
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
168
|
-
const isDevelopment = hmrManager?.isEnabled() ?? false;
|
|
169
|
-
const declaredModules = collectDeclaredModulesInConfig(config);
|
|
170
|
-
const bundleOptions = await this.config.bundleService.createBundleOptions(
|
|
171
|
-
componentName,
|
|
172
|
-
false,
|
|
173
|
-
declaredModules,
|
|
174
|
-
);
|
|
175
|
-
const runtimeImports = this.config.bundleService.getRuntimeImports();
|
|
176
|
-
|
|
177
|
-
const dependencies: AssetDefinition[] = [
|
|
178
|
-
AssetFactory.createFileScript({
|
|
179
|
-
position: 'head',
|
|
180
|
-
filepath: componentFile,
|
|
181
|
-
name: componentName,
|
|
182
|
-
excludeFromHtml: true,
|
|
183
|
-
bundle: true,
|
|
184
|
-
bundleOptions,
|
|
185
|
-
attributes: {
|
|
186
|
-
type: 'module',
|
|
187
|
-
defer: '',
|
|
188
|
-
'data-eco-persist': 'true',
|
|
189
|
-
},
|
|
190
|
-
}),
|
|
191
|
-
AssetFactory.createContentScript({
|
|
192
|
-
position: 'head',
|
|
193
|
-
content: createIslandHydrationScript({
|
|
194
|
-
importPath,
|
|
195
|
-
reactImportPath: runtimeImports.react,
|
|
196
|
-
reactDomClientImportPath: runtimeImports.reactDomClient,
|
|
197
|
-
targetSelector: `[data-eco-component-id="${componentInstanceId}"]`,
|
|
198
|
-
props,
|
|
199
|
-
componentRef: config?.__eco?.id,
|
|
200
|
-
componentFile,
|
|
201
|
-
isDevelopment,
|
|
202
|
-
}),
|
|
203
|
-
name: `${componentName}-hydration`,
|
|
204
|
-
bundle: false,
|
|
205
|
-
attributes: {
|
|
206
|
-
type: 'module',
|
|
207
|
-
defer: '',
|
|
208
|
-
'data-eco-rerun': 'true',
|
|
209
|
-
'data-eco-script-id': `${componentName}-hydration`,
|
|
210
|
-
'data-eco-persist': 'true',
|
|
211
|
-
},
|
|
212
|
-
}),
|
|
213
|
-
];
|
|
214
|
-
|
|
215
|
-
if (!this.config.assetProcessingService) {
|
|
216
|
-
return [];
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
return this.config.assetProcessingService.processDependencies(dependencies, componentName);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Builds all client-side route assets for a page.
|
|
224
|
-
*
|
|
225
|
-
* @param pagePath - Absolute file path of the page
|
|
226
|
-
* @param isMdx - Whether the page is an MDX file
|
|
227
|
-
* @param declaredModules - Explicitly declared browser module specifiers
|
|
228
|
-
* @returns Processed assets for the route
|
|
229
|
-
*/
|
|
230
|
-
async buildRouteRenderAssets(
|
|
231
|
-
pagePath: string,
|
|
232
|
-
isMdx: boolean,
|
|
233
|
-
declaredModules: string[],
|
|
234
|
-
): Promise<ProcessedAsset[]> {
|
|
235
|
-
const componentName = `ecopages-react-${rapidhash(pagePath)}`;
|
|
236
|
-
const hmrManager = this.config.assetProcessingService?.getHmrManager();
|
|
237
|
-
const isDevelopment = hmrManager?.isEnabled() ?? false;
|
|
238
|
-
if (isDevelopment) {
|
|
239
|
-
this.config.hmrPageMetadataCache?.setDeclaredModules(pagePath, declaredModules);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const importPath = await this.resolveAssetImportPath(pagePath, componentName);
|
|
243
|
-
const bundleOptions = await this.config.bundleService.createBundleOptions(
|
|
244
|
-
componentName,
|
|
245
|
-
isMdx,
|
|
246
|
-
declaredModules,
|
|
247
|
-
);
|
|
248
|
-
const dependencies = this.createPageDependencies(
|
|
249
|
-
pagePath,
|
|
250
|
-
componentName,
|
|
251
|
-
importPath,
|
|
252
|
-
bundleOptions,
|
|
253
|
-
isDevelopment,
|
|
254
|
-
isMdx,
|
|
255
|
-
);
|
|
256
|
-
|
|
257
|
-
if (!this.config.assetProcessingService) {
|
|
258
|
-
throw new Error('AssetProcessingService is not set');
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return this.config.assetProcessingService.processDependencies(dependencies, componentName);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Page module loading and configuration resolution service for React integration.
|
|
3
|
-
*
|
|
4
|
-
* Handles MDX compilation, component config metadata resolution,
|
|
5
|
-
* and module hydration analysis.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import { pathToFileURL } from 'node:url';
|
|
12
|
-
import type { EcoComponentConfig, EcoPageFile } from '@ecopages/core';
|
|
13
|
-
import type { BuildExecutor } from '@ecopages/core/build/build-adapter';
|
|
14
|
-
import { rapidhash } from '@ecopages/core/hash';
|
|
15
|
-
import { build } from '@ecopages/core/build/build-adapter';
|
|
16
|
-
import { fileSystem } from '@ecopages/file-system';
|
|
17
|
-
import type { CompileOptions } from '@mdx-js/mdx';
|
|
18
|
-
import { collectDeclaredModulesInConfig } from '../utils/declared-modules.ts';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Configuration for the ReactPageModuleService.
|
|
22
|
-
*/
|
|
23
|
-
export interface ReactPageModuleServiceConfig {
|
|
24
|
-
rootDir: string;
|
|
25
|
-
distDir: string;
|
|
26
|
-
workDir: string;
|
|
27
|
-
buildExecutor: BuildExecutor;
|
|
28
|
-
layoutsDir?: string;
|
|
29
|
-
componentsDir?: string;
|
|
30
|
-
mdxCompilerOptions?: CompileOptions;
|
|
31
|
-
mdxExtensions: string[];
|
|
32
|
-
integrationName: string;
|
|
33
|
-
hasRouterAdapter: boolean;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Manages page module loading (including MDX compilation), config metadata
|
|
38
|
-
* resolution, and hydration analysis for React pages.
|
|
39
|
-
*/
|
|
40
|
-
export class ReactPageModuleService {
|
|
41
|
-
private readonly config: ReactPageModuleServiceConfig;
|
|
42
|
-
|
|
43
|
-
constructor(config: ReactPageModuleServiceConfig) {
|
|
44
|
-
this.config = config;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
49
|
-
* @param filePath - The file path to check
|
|
50
|
-
* @returns True if the file is an MDX file
|
|
51
|
-
*/
|
|
52
|
-
isMdxFile(filePath: string): boolean {
|
|
53
|
-
return this.config.mdxExtensions.some((ext) => filePath.endsWith(ext));
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Compiles and imports an MDX file as a page module.
|
|
58
|
-
*
|
|
59
|
-
* @param filePath - Absolute path to the MDX file
|
|
60
|
-
* @returns The imported module
|
|
61
|
-
*/
|
|
62
|
-
async importMdxPageFile(filePath: string): Promise<unknown> {
|
|
63
|
-
const { createReactMdxLoaderPlugin } = await import('../utils/react-mdx-loader-plugin.ts');
|
|
64
|
-
const mdxPlugin = createReactMdxLoaderPlugin(
|
|
65
|
-
this.config.mdxCompilerOptions ?? {
|
|
66
|
-
jsxImportSource: 'react',
|
|
67
|
-
jsxRuntime: 'automatic',
|
|
68
|
-
development: process?.env?.NODE_ENV === 'development',
|
|
69
|
-
},
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
const outdir = path.join(this.config.workDir, '.server-modules-react-mdx');
|
|
73
|
-
const fileBaseName = path.basename(filePath, path.extname(filePath));
|
|
74
|
-
const fileHash = fileSystem.hash(filePath);
|
|
75
|
-
const cacheBuster = process?.env?.NODE_ENV === 'development' ? `-${Date.now()}` : '';
|
|
76
|
-
const outputFileName = `${fileBaseName}-${fileHash}${cacheBuster}.js`;
|
|
77
|
-
|
|
78
|
-
const buildResult = await build(
|
|
79
|
-
{
|
|
80
|
-
entrypoints: [filePath],
|
|
81
|
-
root: this.config.rootDir,
|
|
82
|
-
outdir,
|
|
83
|
-
target: 'node',
|
|
84
|
-
format: 'esm',
|
|
85
|
-
sourcemap: 'none',
|
|
86
|
-
splitting: false,
|
|
87
|
-
minify: false,
|
|
88
|
-
treeshaking: false,
|
|
89
|
-
naming: outputFileName,
|
|
90
|
-
plugins: [mdxPlugin],
|
|
91
|
-
},
|
|
92
|
-
this.config.buildExecutor,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (!buildResult.success) {
|
|
96
|
-
const details = buildResult.logs.map((log) => log.message).join(' | ');
|
|
97
|
-
throw new Error(`Failed to compile MDX page module: ${details}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const preferredOutputPath = path.join(outdir, outputFileName);
|
|
101
|
-
const compiledOutput =
|
|
102
|
-
buildResult.outputs.find((output) => output.path === preferredOutputPath)?.path ??
|
|
103
|
-
buildResult.outputs.find((output) => output.path.endsWith('.js'))?.path;
|
|
104
|
-
|
|
105
|
-
if (!compiledOutput) {
|
|
106
|
-
throw new Error(`No compiled MDX output generated for page: ${filePath}`);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return await import(pathToFileURL(compiledOutput).href);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
|
|
114
|
-
* Resolves the file path from dependency declarations when not already set.
|
|
115
|
-
*
|
|
116
|
-
* @param config - The component config to augment
|
|
117
|
-
* @param pagePath - Fallback file path if dependency resolution fails
|
|
118
|
-
* @returns Config with `__eco` metadata populated
|
|
119
|
-
*/
|
|
120
|
-
ensureConfigFileMetadata(config: EcoComponentConfig, pagePath: string): EcoComponentConfig {
|
|
121
|
-
if (config.__eco?.file) {
|
|
122
|
-
return config;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const buildEcoMeta = (file: string) => ({
|
|
126
|
-
id: config.__eco?.id ?? rapidhash(file).toString(36),
|
|
127
|
-
integration: config.__eco?.integration ?? this.config.integrationName,
|
|
128
|
-
file,
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
const resolveDependencyValue = (value: string | { src?: string }) =>
|
|
132
|
-
typeof value === 'string' ? value : value.src;
|
|
133
|
-
|
|
134
|
-
const dependencyPaths = [
|
|
135
|
-
...(config.dependencies?.stylesheets ?? []).map(resolveDependencyValue),
|
|
136
|
-
...(config.dependencies?.scripts ?? []).map(resolveDependencyValue),
|
|
137
|
-
]
|
|
138
|
-
.filter((value): value is string => Boolean(value))
|
|
139
|
-
.filter((value) => value.startsWith('./') || value.startsWith('../'));
|
|
140
|
-
|
|
141
|
-
const candidateDirs = [this.config.layoutsDir, this.config.componentsDir, path.dirname(pagePath)].filter(
|
|
142
|
-
(value): value is string => typeof value === 'string' && value.length > 0,
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
for (const dependencyPath of dependencyPaths) {
|
|
146
|
-
for (const candidateDir of candidateDirs) {
|
|
147
|
-
const resolvedDependency = path.resolve(candidateDir, dependencyPath);
|
|
148
|
-
if (fileSystem.exists(resolvedDependency)) {
|
|
149
|
-
return {
|
|
150
|
-
...config,
|
|
151
|
-
__eco: buildEcoMeta(path.join(candidateDir, path.basename(pagePath))),
|
|
152
|
-
};
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
...config,
|
|
159
|
-
__eco: buildEcoMeta(pagePath),
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Recursively checks whether a component config tree declares any browser modules.
|
|
165
|
-
* Used to determine if a page needs hydration.
|
|
166
|
-
*/
|
|
167
|
-
hasModulesInConfig(config: EcoComponentConfig | undefined, visited = new Set<EcoComponentConfig>()): boolean {
|
|
168
|
-
if (!config || visited.has(config)) {
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
visited.add(config);
|
|
173
|
-
|
|
174
|
-
if (config.dependencies?.modules?.some((entry) => entry.trim().length > 0)) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (config.layout?.config && this.hasModulesInConfig(config.layout.config, visited)) {
|
|
179
|
-
return true;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
183
|
-
if (this.hasModulesInConfig(component.config, visited)) {
|
|
184
|
-
return true;
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return false;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Determines whether a page needs client-side hydration.
|
|
193
|
-
*
|
|
194
|
-
* @param pageModule - The imported page module
|
|
195
|
-
* @returns True if the page should be hydrated
|
|
196
|
-
*/
|
|
197
|
-
shouldHydratePage(
|
|
198
|
-
pageModule: EcoPageFile<{ config?: EcoComponentConfig }> & { config?: EcoComponentConfig },
|
|
199
|
-
): boolean {
|
|
200
|
-
if (this.config.hasRouterAdapter) {
|
|
201
|
-
return true;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const pageConfig = pageModule.default?.config;
|
|
205
|
-
return this.hasModulesInConfig(pageConfig) || this.hasModulesInConfig(pageModule.config);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Collects all explicitly declared browser module specifiers from a page module.
|
|
210
|
-
*
|
|
211
|
-
* @param pageModule - The imported page module
|
|
212
|
-
* @returns Deduplicated list of declared module specifiers
|
|
213
|
-
*/
|
|
214
|
-
collectPageDeclaredModules(
|
|
215
|
-
pageModule: EcoPageFile<{ config?: EcoComponentConfig }> & { config?: EcoComponentConfig },
|
|
216
|
-
): string[] {
|
|
217
|
-
const declarations = [
|
|
218
|
-
...collectDeclaredModulesInConfig(pageModule.default?.config),
|
|
219
|
-
...collectDeclaredModulesInConfig(pageModule.config),
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
return Array.from(new Set(declarations));
|
|
223
|
-
}
|
|
224
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Runtime bundle service for React integration.
|
|
3
|
-
*
|
|
4
|
-
* Owns creation of the browser runtime assets for React and React DOM,
|
|
5
|
-
* including shared runtime entry generation and specifier mapping.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
11
|
-
import { createRuntimeSpecifierAliasPlugin } from '@ecopages/core/build/runtime-specifier-alias-plugin';
|
|
12
|
-
import {
|
|
13
|
-
buildBrowserRuntimeAssetUrl,
|
|
14
|
-
createBrowserRuntimeModuleAsset,
|
|
15
|
-
createBrowserRuntimeScriptAsset,
|
|
16
|
-
type AssetDefinition,
|
|
17
|
-
} from '@ecopages/core/services/asset-processing-service';
|
|
18
|
-
import type { ReactRouterAdapter } from '../router-adapter.ts';
|
|
19
|
-
import { createReactDomRuntimeInteropPlugin } from '../utils/react-dom-runtime-interop-plugin.ts';
|
|
20
|
-
import { buildReactRuntimeSpecifierMap } from '../utils/react-runtime-specifier-map.ts';
|
|
21
|
-
|
|
22
|
-
export type ReactRuntimeImports = {
|
|
23
|
-
react: string;
|
|
24
|
-
reactDomClient: string;
|
|
25
|
-
reactJsxRuntime: string;
|
|
26
|
-
reactJsxDevRuntime: string;
|
|
27
|
-
reactDom: string;
|
|
28
|
-
router?: string;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export interface ReactRuntimeBundleServiceConfig {
|
|
32
|
-
routerAdapter?: ReactRouterAdapter;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
type RuntimeMode = 'development' | 'production';
|
|
36
|
-
|
|
37
|
-
export class ReactRuntimeBundleService {
|
|
38
|
-
private readonly config: ReactRuntimeBundleServiceConfig;
|
|
39
|
-
|
|
40
|
-
constructor(config: ReactRuntimeBundleServiceConfig) {
|
|
41
|
-
this.config = config;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private get isDevelopment(): boolean {
|
|
45
|
-
return process.env.NODE_ENV === 'development';
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
private getCurrentRuntimeMode(): RuntimeMode {
|
|
49
|
-
return this.isDevelopment ? 'development' : 'production';
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private createRuntimeDefines(mode: RuntimeMode): Record<string, string> {
|
|
53
|
-
const nodeEnv = JSON.stringify(mode);
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
'process.env.NODE_ENV': nodeEnv,
|
|
57
|
-
'import.meta.env.NODE_ENV': nodeEnv,
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
private getReactVendorFileName(mode: RuntimeMode): string {
|
|
62
|
-
return mode === 'development' ? 'react.development.js' : 'react.js';
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
private getReactDomVendorFileName(mode: RuntimeMode): string {
|
|
66
|
-
return mode === 'development' ? 'react-dom.development.js' : 'react-dom.js';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
private getRouterVendorFileName(mode: RuntimeMode): string {
|
|
70
|
-
if (!this.config.routerAdapter) {
|
|
71
|
-
return '';
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return mode === 'development'
|
|
75
|
-
? `${this.config.routerAdapter.bundle.outputName}.development.js`
|
|
76
|
-
: `${this.config.routerAdapter.bundle.outputName}.js`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
getRuntimeImports(mode = this.getCurrentRuntimeMode()): ReactRuntimeImports {
|
|
80
|
-
const reactVendorFileName = this.getReactVendorFileName(mode);
|
|
81
|
-
const reactDomVendorFileName = this.getReactDomVendorFileName(mode);
|
|
82
|
-
const runtimeImports: ReactRuntimeImports = {
|
|
83
|
-
react: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
84
|
-
reactDomClient: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
85
|
-
reactJsxRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
86
|
-
reactJsxDevRuntime: buildBrowserRuntimeAssetUrl(reactVendorFileName),
|
|
87
|
-
reactDom: buildBrowserRuntimeAssetUrl(reactDomVendorFileName),
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
if (this.config.routerAdapter) {
|
|
91
|
-
runtimeImports.router = buildBrowserRuntimeAssetUrl(this.getRouterVendorFileName(mode));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
return runtimeImports;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
getSpecifierMap(mode = this.getCurrentRuntimeMode()): Record<string, string> {
|
|
98
|
-
return buildReactRuntimeSpecifierMap(this.getRuntimeImports(mode), this.config.routerAdapter);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
getDependencies(): AssetDefinition[] {
|
|
102
|
-
const reactDomRuntimeInteropPlugin = createReactDomRuntimeInteropPlugin();
|
|
103
|
-
const dependencies: AssetDefinition[] = [];
|
|
104
|
-
|
|
105
|
-
for (const mode of ['production', 'development'] as const) {
|
|
106
|
-
const reactRuntimeAliasPlugin = createRuntimeSpecifierAliasPlugin(
|
|
107
|
-
{
|
|
108
|
-
react: buildBrowserRuntimeAssetUrl(this.getReactVendorFileName(mode)),
|
|
109
|
-
},
|
|
110
|
-
{ name: `react-plugin-runtime-specifier-alias-${mode}` },
|
|
111
|
-
);
|
|
112
|
-
const reactDomBundlePlugins = [reactRuntimeAliasPlugin, reactDomRuntimeInteropPlugin].filter(
|
|
113
|
-
(plugin): plugin is EcoBuildPlugin => plugin !== null,
|
|
114
|
-
);
|
|
115
|
-
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(mode);
|
|
116
|
-
const mappedSpecifiers = new Set(Object.keys(this.getSpecifierMap(mode)));
|
|
117
|
-
|
|
118
|
-
dependencies.push(
|
|
119
|
-
createBrowserRuntimeModuleAsset({
|
|
120
|
-
modules: [
|
|
121
|
-
{ specifier: 'react', defaultExport: true },
|
|
122
|
-
{ specifier: 'react/jsx-runtime' },
|
|
123
|
-
{ specifier: 'react/jsx-dev-runtime' },
|
|
124
|
-
],
|
|
125
|
-
name: 'react',
|
|
126
|
-
fileName: this.getReactVendorFileName(mode),
|
|
127
|
-
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
128
|
-
bundleOptions: {
|
|
129
|
-
define: this.createRuntimeDefines(mode),
|
|
130
|
-
},
|
|
131
|
-
}),
|
|
132
|
-
createBrowserRuntimeModuleAsset({
|
|
133
|
-
modules: [{ specifier: 'react-dom', defaultExport: true }, { specifier: 'react-dom/client' }],
|
|
134
|
-
name: 'react-dom',
|
|
135
|
-
fileName: this.getReactDomVendorFileName(mode),
|
|
136
|
-
cacheDirName: `ecopages-react-runtime-${mode}`,
|
|
137
|
-
bundleOptions: {
|
|
138
|
-
define: this.createRuntimeDefines(mode),
|
|
139
|
-
plugins: reactDomBundlePlugins,
|
|
140
|
-
},
|
|
141
|
-
}),
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
if (this.config.routerAdapter) {
|
|
145
|
-
const unresolvedExternals = this.config.routerAdapter.bundle.externals.filter(
|
|
146
|
-
(external) => !mappedSpecifiers.has(external),
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
dependencies.push(
|
|
150
|
-
createBrowserRuntimeScriptAsset({
|
|
151
|
-
importPath: this.config.routerAdapter.bundle.importPath,
|
|
152
|
-
name: this.config.routerAdapter.bundle.outputName,
|
|
153
|
-
fileName: this.getRouterVendorFileName(mode),
|
|
154
|
-
bundleOptions: {
|
|
155
|
-
define: this.createRuntimeDefines(mode),
|
|
156
|
-
external: unresolvedExternals,
|
|
157
|
-
plugins: [runtimeAliasPlugin],
|
|
158
|
-
},
|
|
159
|
-
}),
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return dependencies;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
createRuntimeAliasPlugin(mode = this.getCurrentRuntimeMode()): EcoBuildPlugin {
|
|
168
|
-
return createRuntimeSpecifierAliasPlugin(this.getSpecifierMap(mode), {
|
|
169
|
-
name: `react-plugin-runtime-alias-${mode}`,
|
|
170
|
-
})!;
|
|
171
|
-
}
|
|
172
|
-
}
|