@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -29
- package/package.json +24 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +65 -43
- package/src/react-hmr-strategy.js +298 -145
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +40 -111
- package/src/react.plugin.js +136 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/runtime/use-sync-external-store-with-selector.d.ts +3 -0
- package/src/runtime/use-sync-external-store-with-selector.js +56 -0
- package/src/services/react-bundle.service.d.ts +22 -35
- package/src/services/react-bundle.service.js +41 -105
- package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
- package/src/services/react-hmr-page-metadata-cache.js +18 -2
- package/src/services/react-hydration-asset.service.d.ts +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
- package/src/services/react-mdx-config-dependency.service.js +122 -0
- package/src/services/react-page-module.service.d.ts +10 -2
- package/src/services/react-page-module.service.js +47 -39
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +20 -13
- package/src/services/react-runtime-bundle.service.js +146 -179
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +27 -6
- package/src/utils/hydration-scripts.js +177 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +38 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +8 -0
- package/src/utils/react-runtime-alias-map.js +90 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -217
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -710
- package/src/utils/client-only.ts +0 -27
- package/src/utils/declared-modules.ts +0 -99
- package/src/utils/dynamic.ts +0 -27
- package/src/utils/hmr-scripts.ts +0 -47
- package/src/utils/html-boundary.ts +0 -66
- package/src/utils/hydration-scripts.ts +0 -338
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
package/src/react-renderer.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
IntegrationRenderer
|
|
3
|
+
} from "@ecopages/core/route-renderer/integration-renderer";
|
|
3
4
|
import { RESOLVED_ASSETS_DIR } from "@ecopages/core/constants";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
5
|
+
import { getAppBuildExecutor } from "@ecopages/core/build/build-adapter";
|
|
6
|
+
import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { REACT_PLUGIN_NAME } from "./react.constants.js";
|
|
8
10
|
import { hasSingleRootElement } from "./utils/html-boundary.js";
|
|
9
11
|
import { ReactBundleService } from "./services/react-bundle.service.js";
|
|
10
12
|
import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
|
|
13
|
+
import { ReactMdxConfigDependencyService } from "./services/react-mdx-config-dependency.service.js";
|
|
11
14
|
import { ReactPageModuleService } from "./services/react-page-module.service.js";
|
|
12
|
-
import {
|
|
15
|
+
import { ReactPagePayloadService } from "./services/react-page-payload.service.js";
|
|
16
|
+
import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
|
|
13
17
|
class ReactRenderError extends Error {
|
|
14
18
|
constructor(message) {
|
|
15
19
|
super(message);
|
|
@@ -17,103 +21,417 @@ class ReactRenderError extends Error {
|
|
|
17
21
|
}
|
|
18
22
|
}
|
|
19
23
|
class BundleError extends Error {
|
|
24
|
+
logs;
|
|
20
25
|
constructor(message, logs) {
|
|
21
26
|
super(message);
|
|
22
|
-
this.logs = logs;
|
|
23
27
|
this.name = "BundleError";
|
|
28
|
+
this.logs = logs;
|
|
24
29
|
}
|
|
25
30
|
}
|
|
26
31
|
class ReactRenderer extends IntegrationRenderer {
|
|
27
|
-
name =
|
|
32
|
+
name = REACT_PLUGIN_NAME;
|
|
28
33
|
componentDirectory = RESOLVED_ASSETS_DIR;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
reactRuntimeModules;
|
|
35
|
+
routerAdapter;
|
|
36
|
+
mdxCompilerOptions;
|
|
37
|
+
mdxExtensions;
|
|
38
|
+
hmrPageMetadataCache;
|
|
34
39
|
/**
|
|
35
40
|
* Enables explicit graph behavior for React page-entry bundling.
|
|
36
41
|
*
|
|
37
42
|
* When true, page-entry bundles disable AST server-only stripping and rely
|
|
38
43
|
* on explicit dependency declarations for browser graph composition.
|
|
39
44
|
*/
|
|
40
|
-
|
|
45
|
+
explicitGraphEnabled;
|
|
41
46
|
/** @internal */
|
|
42
47
|
bundleService;
|
|
43
48
|
/** @internal */
|
|
44
49
|
pageModuleService;
|
|
45
50
|
/** @internal */
|
|
46
51
|
hydrationAssetService;
|
|
52
|
+
/** @internal */
|
|
53
|
+
pagePayloadService;
|
|
54
|
+
/** @internal */
|
|
55
|
+
mdxConfigDependencyService;
|
|
47
56
|
constructor(options) {
|
|
48
|
-
|
|
57
|
+
const { reactConfig, ...rendererOptions } = options;
|
|
58
|
+
super(rendererOptions);
|
|
59
|
+
this.routerAdapter = reactConfig?.routerAdapter;
|
|
60
|
+
this.mdxCompilerOptions = reactConfig?.mdxCompilerOptions;
|
|
61
|
+
this.mdxExtensions = reactConfig?.mdxExtensions ?? [".mdx"];
|
|
62
|
+
this.hmrPageMetadataCache = reactConfig?.hmrPageMetadataCache;
|
|
63
|
+
this.explicitGraphEnabled = reactConfig?.explicitGraphEnabled ?? false;
|
|
49
64
|
this.bundleService = new ReactBundleService({
|
|
50
65
|
rootDir: this.appConfig.rootDir,
|
|
51
|
-
routerAdapter:
|
|
52
|
-
mdxCompilerOptions:
|
|
66
|
+
routerAdapter: this.routerAdapter,
|
|
67
|
+
mdxCompilerOptions: this.mdxCompilerOptions,
|
|
68
|
+
jsxImportSource: (this.appConfig.integrations ?? []).find((integration) => integration.name === this.name)?.jsxImportSource,
|
|
69
|
+
nonReactExtensions: (this.appConfig.integrations ?? []).filter((integration) => integration.name !== this.name).flatMap((integration) => integration.extensions)
|
|
53
70
|
});
|
|
54
71
|
this.pageModuleService = new ReactPageModuleService({
|
|
55
72
|
rootDir: this.appConfig.rootDir,
|
|
56
73
|
distDir: this.appConfig.absolutePaths.distDir,
|
|
74
|
+
workDir: this.appConfig.absolutePaths.workDir,
|
|
75
|
+
buildExecutor: getAppBuildExecutor(this.appConfig),
|
|
57
76
|
layoutsDir: this.appConfig.absolutePaths.layoutsDir,
|
|
58
77
|
componentsDir: this.appConfig.absolutePaths.componentsDir,
|
|
59
|
-
mdxCompilerOptions:
|
|
60
|
-
mdxExtensions:
|
|
78
|
+
mdxCompilerOptions: this.mdxCompilerOptions,
|
|
79
|
+
mdxExtensions: this.mdxExtensions,
|
|
61
80
|
integrationName: this.name,
|
|
62
|
-
hasRouterAdapter: Boolean(
|
|
81
|
+
hasRouterAdapter: Boolean(this.routerAdapter)
|
|
63
82
|
});
|
|
64
83
|
this.hydrationAssetService = new ReactHydrationAssetService({
|
|
65
84
|
srcDir: this.appConfig.srcDir,
|
|
66
|
-
routerAdapter:
|
|
85
|
+
routerAdapter: this.routerAdapter,
|
|
67
86
|
assetProcessingService: this.assetProcessingService,
|
|
68
87
|
bundleService: this.bundleService,
|
|
69
|
-
hmrPageMetadataCache:
|
|
88
|
+
hmrPageMetadataCache: this.hmrPageMetadataCache
|
|
89
|
+
});
|
|
90
|
+
this.pagePayloadService = new ReactPagePayloadService();
|
|
91
|
+
this.mdxConfigDependencyService = new ReactMdxConfigDependencyService({
|
|
92
|
+
integrationName: this.name,
|
|
93
|
+
pageModuleService: this.pageModuleService,
|
|
94
|
+
assetProcessingService: this.assetProcessingService
|
|
70
95
|
});
|
|
71
96
|
}
|
|
72
97
|
shouldRenderPageComponent() {
|
|
73
98
|
return false;
|
|
74
99
|
}
|
|
75
100
|
/**
|
|
76
|
-
*
|
|
101
|
+
* Reads the declared integration name for a component or layout.
|
|
77
102
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
82
|
-
|
|
103
|
+
* We honor both the explicit `config.integration` override and injected
|
|
104
|
+
* `config.__eco.integration` metadata because pages can arrive here through
|
|
105
|
+
* authored config as well as build-time component metadata.
|
|
106
|
+
*/
|
|
107
|
+
getComponentIntegration(component) {
|
|
108
|
+
return component?.config?.integration ?? component?.config?.__eco?.integration;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Returns whether a component should stay inside the React render lane.
|
|
83
112
|
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
113
|
+
* Components without explicit integration metadata are treated as React-owned
|
|
114
|
+
* here because this renderer only receives them after the route pipeline has
|
|
115
|
+
* already selected the React integration.
|
|
86
116
|
*/
|
|
87
|
-
|
|
88
|
-
const
|
|
117
|
+
isReactManagedComponent(component) {
|
|
118
|
+
const integration = this.getComponentIntegration(component);
|
|
119
|
+
return integration === void 0 || integration === this.name;
|
|
120
|
+
}
|
|
121
|
+
getComponentRequires(component) {
|
|
122
|
+
return component?.requires;
|
|
123
|
+
}
|
|
124
|
+
getRouterDocumentAttributes() {
|
|
125
|
+
if (!this.routerAdapter) {
|
|
126
|
+
return void 0;
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
[ECO_DOCUMENT_OWNER_ATTRIBUTE]: "react-router"
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Commits a framework-agnostic component to React semantics.
|
|
134
|
+
*
|
|
135
|
+
* This is one of the two real cast boundaries in this file. Core keeps
|
|
136
|
+
* `EcoComponent` broad so integrations can share the same public surface; once
|
|
137
|
+
* the React renderer is executing, `createElement()` needs a concrete React
|
|
138
|
+
* component signature.
|
|
139
|
+
*/
|
|
140
|
+
asReactComponent(component) {
|
|
141
|
+
return component;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Commits a mixed-shell component to the string-returning contract required by
|
|
145
|
+
* non-React layouts and HTML templates.
|
|
146
|
+
*
|
|
147
|
+
* This is the second real cast boundary: once we decide a shell is not managed
|
|
148
|
+
* by React, we call it directly and require serialized HTML back.
|
|
149
|
+
*/
|
|
150
|
+
asNonReactShellComponent(component) {
|
|
151
|
+
return component;
|
|
152
|
+
}
|
|
153
|
+
resolveReactRuntimeModules() {
|
|
154
|
+
const appPackageJsonPath = path.resolve(this.appConfig.rootDir || process.cwd(), "package.json");
|
|
155
|
+
try {
|
|
156
|
+
const requireFromApp = createRequire(appPackageJsonPath);
|
|
157
|
+
return {
|
|
158
|
+
react: requireFromApp("react"),
|
|
159
|
+
reactDomServer: requireFromApp("react-dom/server")
|
|
160
|
+
};
|
|
161
|
+
} catch {
|
|
162
|
+
const requireFromIntegration = createRequire(import.meta.url);
|
|
163
|
+
return {
|
|
164
|
+
react: requireFromIntegration("react"),
|
|
165
|
+
reactDomServer: requireFromIntegration("react-dom/server")
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
getReactRuntimeModules() {
|
|
170
|
+
this.reactRuntimeModules ??= this.resolveReactRuntimeModules();
|
|
171
|
+
return this.reactRuntimeModules;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Appends route hydration assets for a concrete page/view file to the current
|
|
175
|
+
* HTML transformer state.
|
|
176
|
+
*/
|
|
177
|
+
async appendHydrationAssetsForFile(filePath) {
|
|
178
|
+
if (!filePath) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const pageBrowserGraph = await this.resolvePageBrowserGraphForFile(filePath);
|
|
182
|
+
this.mergePageBrowserGraphIntoPagePackage(pageBrowserGraph);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
186
|
+
* return serialized HTML.
|
|
187
|
+
*
|
|
188
|
+
* The React renderer can compose through another integration's shell, but only
|
|
189
|
+
* if that shell yields a string that can be inserted into the final document.
|
|
190
|
+
*/
|
|
191
|
+
async renderNonReactShellComponent(Component, props, label) {
|
|
192
|
+
const output = await Component(props);
|
|
193
|
+
if (typeof output === "string") {
|
|
194
|
+
return output;
|
|
195
|
+
}
|
|
196
|
+
throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Renders one React component while preserving already-resolved child HTML.
|
|
200
|
+
*
|
|
201
|
+
* When nested foreign-subtree resolution has already produced child HTML for this
|
|
202
|
+
* component, the child payload must remain raw SSR output rather than a React
|
|
203
|
+
* string child, otherwise React would escape it. This helper renders a unique
|
|
204
|
+
* token through React and swaps that token back to the resolved HTML
|
|
205
|
+
* afterward.
|
|
206
|
+
*
|
|
207
|
+
* @param input Component render input for the current render step.
|
|
208
|
+
* @param context React-specific render context for stable token generation.
|
|
209
|
+
* @returns Serialized component HTML with resolved child markup preserved.
|
|
210
|
+
*/
|
|
211
|
+
renderComponentHtml(input, context, runtimeContext) {
|
|
212
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
213
|
+
if (input.children === void 0) {
|
|
214
|
+
return this.normalizeUnresolvedMarkerArtifactHtml(
|
|
215
|
+
reactDomServer.renderToString(react.createElement(this.asReactComponent(input.component), input.props))
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
|
|
219
|
+
const rawChildrenToken = `__ECO_RAW_HTML_CHILD_${context.componentInstanceId ?? "component"}__`;
|
|
220
|
+
if (runtimeContext) {
|
|
221
|
+
runtimeContext.rawChildrenToken = rawChildrenToken;
|
|
222
|
+
runtimeContext.rawChildrenHtml = resolvedChildHtml;
|
|
223
|
+
}
|
|
224
|
+
const html = reactDomServer.renderToString(
|
|
225
|
+
react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
226
|
+
);
|
|
227
|
+
return this.normalizeUnresolvedMarkerArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
231
|
+
*
|
|
232
|
+
* Queued foreign-subtree resolution may render children through a fragment path before all
|
|
233
|
+
* nested integration tokens are resolved. When that happens, React must never see
|
|
234
|
+
* the resolved child HTML as a normal string child or it would escape it. The
|
|
235
|
+
* runtime context stores the placeholder token and the raw child HTML so the
|
|
236
|
+
* fragment render path can reinsert it before foreign-subtree tokens are handled.
|
|
237
|
+
*/
|
|
238
|
+
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
239
|
+
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
240
|
+
return html;
|
|
241
|
+
}
|
|
242
|
+
return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Renders queued child content through React and then resolves nested foreign-subtree tokens.
|
|
246
|
+
*
|
|
247
|
+
* This path is only used for children that were deferred while React rendered the
|
|
248
|
+
* parent component. It first restores any raw child HTML placeholders owned by the
|
|
249
|
+
* current runtime context, then asks the shared queued foreign-subtree resolver to swap
|
|
250
|
+
* foreign integration tokens with their resolved HTML.
|
|
251
|
+
*/
|
|
252
|
+
async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
|
|
253
|
+
if (children === void 0) {
|
|
254
|
+
return void 0;
|
|
255
|
+
}
|
|
256
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
257
|
+
let html = this.normalizeUnresolvedMarkerArtifactHtml(
|
|
258
|
+
reactDomServer.renderToString(react.createElement(react.Fragment, null, children))
|
|
259
|
+
);
|
|
260
|
+
html = this.restoreRuntimeChildHtml(html, runtimeContext);
|
|
261
|
+
html = await this.foreignSubtreeExecutionService.resolveQueuedTokens(
|
|
262
|
+
html,
|
|
263
|
+
queuedResolutionsByToken,
|
|
264
|
+
resolveToken
|
|
265
|
+
);
|
|
266
|
+
return html;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
|
|
270
|
+
*
|
|
271
|
+
* React components can enqueue nested foreign subtrees while the parent HTML is being
|
|
272
|
+
* rendered. This delegates to the shared renderer-owned queue resolver but keeps
|
|
273
|
+
* the React-specific child rendering behavior local so raw child HTML and React's
|
|
274
|
+
* fragment rendering semantics stay coordinated.
|
|
275
|
+
*/
|
|
276
|
+
async resolveQueuedForeignSubtreeHtml(html, runtimeContext) {
|
|
277
|
+
return this.foreignSubtreeExecutionService.resolveQueuedHtml({
|
|
278
|
+
currentIntegrationName: this.name,
|
|
279
|
+
html,
|
|
280
|
+
runtimeContext,
|
|
281
|
+
queueLabel: "React",
|
|
282
|
+
getOwningRenderer: (integrationName, rendererCache) => this.getIntegrationRendererForName(integrationName, rendererCache),
|
|
283
|
+
applyAttributesToFirstElement: (resolvedHtml, attributes) => this.htmlTransformer.applyAttributesToFirstElement(resolvedHtml, attributes),
|
|
284
|
+
dedupeProcessedAssets: (assets) => this.htmlTransformer.dedupeProcessedAssets(assets),
|
|
285
|
+
renderQueuedChildren: async (children, currentRuntimeContext, queuedResolutionsByToken, resolveToken) => {
|
|
286
|
+
const renderedHtml = await this.renderQueuedChildrenToHtml(
|
|
287
|
+
children,
|
|
288
|
+
currentRuntimeContext,
|
|
289
|
+
queuedResolutionsByToken,
|
|
290
|
+
resolveToken
|
|
291
|
+
);
|
|
292
|
+
return {
|
|
293
|
+
assets: [],
|
|
294
|
+
html: renderedHtml
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
buildHydrationProps(props) {
|
|
300
|
+
if (!props || !Object.prototype.hasOwnProperty.call(props, "locals")) {
|
|
301
|
+
return props ?? {};
|
|
302
|
+
}
|
|
303
|
+
const { locals: _locals, ...hydrationProps } = props;
|
|
304
|
+
return hydrationProps;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Builds shared document html contributions for router-backed React pages rendered
|
|
308
|
+
* through a non-React HTML shell.
|
|
309
|
+
*/
|
|
310
|
+
buildNonReactDocumentContributions(htmlTemplate, pageProps) {
|
|
311
|
+
if (this.isReactManagedComponent(htmlTemplate) || !this.routerAdapter) {
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
return [
|
|
315
|
+
{
|
|
316
|
+
placement: "head-append",
|
|
317
|
+
html: this.pagePayloadService.buildRouterPageDataScript(pageProps)
|
|
318
|
+
}
|
|
319
|
+
];
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Renders a foreign integration component that participates in React composition.
|
|
323
|
+
*
|
|
324
|
+
* Non-React components must resolve to serialized HTML so React can embed them as
|
|
325
|
+
* mixed-shell children. Any component-owned dependencies still need to flow
|
|
326
|
+
* through the shared dependency resolver before queued foreign-subtree tokens are finalized.
|
|
327
|
+
*/
|
|
328
|
+
async renderForeignComponentWithSerializedHtml(input, runtimeContext) {
|
|
329
|
+
let props = input.props;
|
|
330
|
+
if (input.children !== void 0) {
|
|
331
|
+
props = {
|
|
332
|
+
...input.props,
|
|
333
|
+
children: typeof input.children === "string" ? input.children : String(input.children ?? "")
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
const html = await this.renderNonReactShellComponent(
|
|
337
|
+
this.asNonReactShellComponent(input.component),
|
|
338
|
+
props,
|
|
339
|
+
"Component"
|
|
340
|
+
);
|
|
341
|
+
const hasDependencies = Boolean(input.component.config?.dependencies);
|
|
342
|
+
const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
|
|
343
|
+
const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
|
|
344
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
|
|
345
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
346
|
+
...assets ?? [],
|
|
347
|
+
...queuedForeignSubtreeResolution.assets
|
|
348
|
+
]);
|
|
349
|
+
return {
|
|
350
|
+
html: queuedForeignSubtreeResolution.html,
|
|
351
|
+
canAttachAttributes: true,
|
|
352
|
+
rootTag: this.getRootTagName(queuedForeignSubtreeResolution.html),
|
|
353
|
+
integrationName: this.name,
|
|
354
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Renders a React-owned component and attaches island hydration metadata when possible.
|
|
359
|
+
*
|
|
360
|
+
* This path keeps React-owned SSR, queued foreign-subtree resolution, and optional
|
|
361
|
+
* island hydration wiring together so the public `renderComponent()` method can
|
|
362
|
+
* read as orchestration rather than implementation detail.
|
|
363
|
+
*/
|
|
364
|
+
async renderReactManagedComponent(input, runtimeContext) {
|
|
89
365
|
const componentConfig = input.component.config;
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
366
|
+
const context = {
|
|
367
|
+
componentInstanceId: input.integrationContext?.componentInstanceId
|
|
368
|
+
};
|
|
369
|
+
const hasResolvedChildHtml = input.children !== void 0;
|
|
370
|
+
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
371
|
+
const queuedForeignSubtreeResolution = await this.resolveQueuedForeignSubtreeHtml(html, runtimeContext);
|
|
372
|
+
html = queuedForeignSubtreeResolution.html;
|
|
373
|
+
const canAttachAttributes = hasSingleRootElement(html);
|
|
374
|
+
const rootTag = this.getRootTagName(html);
|
|
94
375
|
const componentFile = componentConfig?.__eco?.file;
|
|
95
|
-
const context = input.integrationContext ?? {};
|
|
96
376
|
let rootAttributes;
|
|
97
377
|
let assets;
|
|
98
|
-
if (canAttachAttributes && componentFile && this.assetProcessingService) {
|
|
99
|
-
const componentInstanceId = context.componentInstanceId
|
|
100
|
-
assets = await this.hydrationAssetService.buildComponentRenderAssets(
|
|
101
|
-
|
|
102
|
-
componentInstanceId,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
rootAttributes = { "data-eco-component-id": componentInstanceId };
|
|
378
|
+
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService && !hasResolvedChildHtml) {
|
|
379
|
+
const componentInstanceId = context.componentInstanceId;
|
|
380
|
+
assets = await this.hydrationAssetService.buildComponentRenderAssets(componentFile, componentConfig);
|
|
381
|
+
rootAttributes = {
|
|
382
|
+
"data-eco-component-id": componentInstanceId,
|
|
383
|
+
"data-eco-component-key": getReactIslandComponentKey(componentFile, componentConfig),
|
|
384
|
+
"data-eco-props": btoa(JSON.stringify(this.buildHydrationProps(input.props)))
|
|
385
|
+
};
|
|
107
386
|
}
|
|
387
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
388
|
+
...assets ?? [],
|
|
389
|
+
...queuedForeignSubtreeResolution.assets
|
|
390
|
+
]);
|
|
108
391
|
return {
|
|
109
392
|
html,
|
|
110
393
|
canAttachAttributes,
|
|
111
394
|
rootTag,
|
|
112
395
|
integrationName: this.name,
|
|
113
396
|
rootAttributes,
|
|
114
|
-
assets
|
|
397
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
115
398
|
};
|
|
116
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Renders a React component for component-level orchestration.
|
|
402
|
+
*
|
|
403
|
+
* Behavior:
|
|
404
|
+
* - SSR always returns the component's own root HTML (no synthetic wrapper).
|
|
405
|
+
* - When an explicit component instance id is provided, a stable
|
|
406
|
+
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
407
|
+
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
408
|
+
* - When resolved child HTML is provided, that foreign subtree is treated as a pure SSR
|
|
409
|
+
* composition step and does not emit hydration assets for the parent wrapper.
|
|
410
|
+
*
|
|
411
|
+
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
412
|
+
* deterministic mount target per component instance.
|
|
413
|
+
*/
|
|
414
|
+
async renderComponent(input) {
|
|
415
|
+
const runtimeContext = this.getQueuedForeignSubtreeResolutionContext(input);
|
|
416
|
+
if (!this.isReactManagedComponent(input.component)) {
|
|
417
|
+
return this.renderForeignComponentWithSerializedHtml(input, runtimeContext);
|
|
418
|
+
}
|
|
419
|
+
return this.renderReactManagedComponent(input, runtimeContext);
|
|
420
|
+
}
|
|
421
|
+
createForeignChildRuntime(options) {
|
|
422
|
+
return this.createQueuedForeignSubtreeExecutionRuntime({
|
|
423
|
+
renderInput: options.renderInput,
|
|
424
|
+
rendererCache: options.rendererCache,
|
|
425
|
+
createRuntimeContext: (integrationContext, rendererCache) => ({
|
|
426
|
+
rendererCache,
|
|
427
|
+
componentInstanceScope: integrationContext.componentInstanceId,
|
|
428
|
+
nextForeignSubtreeId: 0,
|
|
429
|
+
queuedResolutions: [],
|
|
430
|
+
rawChildrenToken: void 0,
|
|
431
|
+
rawChildrenHtml: void 0
|
|
432
|
+
})
|
|
433
|
+
});
|
|
434
|
+
}
|
|
117
435
|
/**
|
|
118
436
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
119
437
|
* @param filePath - The file path to check
|
|
@@ -122,47 +440,49 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
122
440
|
isMdxFile(filePath) {
|
|
123
441
|
return this.pageModuleService.isMdxFile(filePath);
|
|
124
442
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
if (
|
|
135
|
-
|
|
136
|
-
components.push({ config: layoutConfig });
|
|
137
|
-
}
|
|
138
|
-
if (config?.dependencies) {
|
|
139
|
-
const configWithMeta = {
|
|
140
|
-
...config,
|
|
141
|
-
__eco: { id: rapidhash(pagePath).toString(36), file: pagePath, integration: "react" }
|
|
142
|
-
};
|
|
143
|
-
components.push({ config: configWithMeta });
|
|
443
|
+
usesIntegrationPageImporter(file) {
|
|
444
|
+
return this.pageModuleService.isMdxFile(file);
|
|
445
|
+
}
|
|
446
|
+
async importIntegrationPageFile(file, options) {
|
|
447
|
+
return await this.pageModuleService.importMdxPageFile(file, options);
|
|
448
|
+
}
|
|
449
|
+
normalizeImportedPageFile(file, pageModule) {
|
|
450
|
+
const reactModule = pageModule;
|
|
451
|
+
const { default: Page, getMetadata, config } = reactModule;
|
|
452
|
+
if (this.pageModuleService.isMdxFile(file) && config) {
|
|
453
|
+
Page.config = config;
|
|
144
454
|
}
|
|
145
|
-
return
|
|
455
|
+
return {
|
|
456
|
+
...pageModule,
|
|
457
|
+
default: Page,
|
|
458
|
+
getMetadata,
|
|
459
|
+
config
|
|
460
|
+
};
|
|
146
461
|
}
|
|
147
|
-
async
|
|
462
|
+
async collectPageBrowserGraphContribution(context) {
|
|
148
463
|
try {
|
|
149
|
-
const pageModule =
|
|
150
|
-
const shouldHydrate =
|
|
464
|
+
const { file: pagePath, pageModule } = context;
|
|
465
|
+
const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
|
|
151
466
|
if (!shouldHydrate) {
|
|
152
|
-
return [];
|
|
467
|
+
return { assets: [] };
|
|
153
468
|
}
|
|
154
469
|
const isMdx = this.pageModuleService.isMdxFile(pagePath);
|
|
155
470
|
const declaredModules = this.pageModuleService.collectPageDeclaredModules(pageModule);
|
|
156
|
-
const
|
|
471
|
+
const dependencies = await this.hydrationAssetService.createPageBrowserGraphDependencies(
|
|
157
472
|
pagePath,
|
|
158
473
|
isMdx,
|
|
159
474
|
declaredModules
|
|
160
475
|
);
|
|
476
|
+
const assets = [];
|
|
161
477
|
if (isMdx) {
|
|
162
|
-
const mdxConfigAssets = await this.processMdxConfigDependencies(
|
|
163
|
-
|
|
478
|
+
const mdxConfigAssets = await this.mdxConfigDependencyService.processMdxConfigDependencies({
|
|
479
|
+
pagePath,
|
|
480
|
+
config: pageModule.config,
|
|
481
|
+
processComponentDependencies: async (components) => await this.processComponentDependencies(components)
|
|
482
|
+
});
|
|
483
|
+
assets.push(...mdxConfigAssets);
|
|
164
484
|
}
|
|
165
|
-
return
|
|
485
|
+
return { dependencies, assets };
|
|
166
486
|
} catch (error) {
|
|
167
487
|
if (error instanceof BundleError) {
|
|
168
488
|
console.error("[ecopages] Bundle errors:", error.logs);
|
|
@@ -172,18 +492,14 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
172
492
|
);
|
|
173
493
|
}
|
|
174
494
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
getMetadata,
|
|
184
|
-
config
|
|
185
|
-
};
|
|
186
|
-
}
|
|
495
|
+
/**
|
|
496
|
+
* Renders a full route response for the filesystem page pipeline.
|
|
497
|
+
*
|
|
498
|
+
* This path receives already-resolved route metadata, layout, locals, and HTML
|
|
499
|
+
* template instances from the shared renderer orchestration. Its main job is to
|
|
500
|
+
* serialize only the browser-safe page payload, compose the mixed React/non-
|
|
501
|
+
* React shell tree, and hand the result back as a document body.
|
|
502
|
+
*/
|
|
187
503
|
async render({
|
|
188
504
|
params,
|
|
189
505
|
query,
|
|
@@ -197,99 +513,103 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
197
513
|
pageProps
|
|
198
514
|
}) {
|
|
199
515
|
try {
|
|
200
|
-
const
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
const allPageProps = {
|
|
204
|
-
...pageProps,
|
|
516
|
+
const safeLocals = this.pagePayloadService.getSerializableLocals(locals, this.getComponentRequires(Page));
|
|
517
|
+
const allPageProps = this.pagePayloadService.buildSerializedPageProps({
|
|
518
|
+
pageProps,
|
|
205
519
|
params,
|
|
206
520
|
query,
|
|
207
|
-
|
|
208
|
-
};
|
|
209
|
-
return await
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
521
|
+
safeLocals
|
|
522
|
+
});
|
|
523
|
+
return await this.renderPageWithDocumentShell({
|
|
524
|
+
page: {
|
|
525
|
+
component: Page,
|
|
526
|
+
props: { params, query, ...props, locals: pageLocals }
|
|
527
|
+
},
|
|
528
|
+
layout: Layout ? {
|
|
529
|
+
component: Layout,
|
|
530
|
+
props: locals ? { locals } : {}
|
|
531
|
+
} : void 0,
|
|
532
|
+
htmlTemplate: HtmlTemplate,
|
|
533
|
+
metadata,
|
|
534
|
+
pageProps: allPageProps
|
|
535
|
+
});
|
|
219
536
|
} catch (error) {
|
|
220
537
|
throw this.createRenderError("Failed to render component", error);
|
|
221
538
|
}
|
|
222
539
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
*
|
|
226
|
-
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
227
|
-
* request-scoped data (e.g., session). This data needs to be serialized to the
|
|
228
|
-
* client for hydration to match the server-rendered output.
|
|
229
|
-
*
|
|
230
|
-
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
231
|
-
* to prevent accidental use. This method safely detects that case and returns
|
|
232
|
-
* `undefined` instead of throwing.
|
|
233
|
-
*
|
|
234
|
-
* @param locals - The locals object from the render context
|
|
235
|
-
* @returns The locals object if serializable, undefined otherwise
|
|
236
|
-
*/
|
|
237
|
-
getSerializableLocals(locals) {
|
|
238
|
-
try {
|
|
239
|
-
if (locals && Object.keys(locals).length > 0) {
|
|
240
|
-
return locals;
|
|
241
|
-
}
|
|
540
|
+
getHtmlDocumentContributions(options) {
|
|
541
|
+
if (options.partial || !options.renderOptions) {
|
|
242
542
|
return void 0;
|
|
243
|
-
} catch (e) {
|
|
244
|
-
if (e instanceof LocalsAccessError) {
|
|
245
|
-
return void 0;
|
|
246
|
-
}
|
|
247
|
-
throw e;
|
|
248
543
|
}
|
|
544
|
+
const safeLocals = this.pagePayloadService.getSerializableLocals(
|
|
545
|
+
options.renderOptions.locals,
|
|
546
|
+
this.getComponentRequires(options.renderOptions.Page)
|
|
547
|
+
);
|
|
548
|
+
const allPageProps = this.pagePayloadService.buildSerializedPageProps({
|
|
549
|
+
pageProps: options.renderOptions.pageProps,
|
|
550
|
+
params: options.renderOptions.params,
|
|
551
|
+
query: options.renderOptions.query,
|
|
552
|
+
safeLocals
|
|
553
|
+
});
|
|
554
|
+
return this.buildNonReactDocumentContributions(options.renderOptions.HtmlTemplate, allPageProps);
|
|
555
|
+
}
|
|
556
|
+
getDocumentAttributes() {
|
|
557
|
+
return this.getRouterDocumentAttributes();
|
|
249
558
|
}
|
|
559
|
+
/**
|
|
560
|
+
* Renders an arbitrary React view through the application's HTML shell.
|
|
561
|
+
*
|
|
562
|
+
* Unlike route rendering, this path starts from a single component rather than a
|
|
563
|
+
* page module discovered by the router. It still needs to resolve metadata,
|
|
564
|
+
* layout dependencies, and hydration assets so direct `ctx.render()` calls match
|
|
565
|
+
* normal page responses.
|
|
566
|
+
*/
|
|
250
567
|
async renderToResponse(view, props, ctx) {
|
|
251
568
|
try {
|
|
569
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
252
570
|
const viewConfig = view.config;
|
|
253
571
|
const Layout = viewConfig?.layout;
|
|
254
|
-
const ViewComponent = view;
|
|
255
|
-
const
|
|
572
|
+
const ViewComponent = this.asReactComponent(view);
|
|
573
|
+
const normalizedProps = props ?? {};
|
|
256
574
|
if (ctx.partial) {
|
|
257
|
-
|
|
258
|
-
|
|
575
|
+
return this.renderPartialViewResponse({
|
|
576
|
+
view,
|
|
577
|
+
props,
|
|
578
|
+
ctx,
|
|
579
|
+
renderInline: async () => await reactDomServer.renderToReadableStream(
|
|
580
|
+
react.createElement(ViewComponent, normalizedProps)
|
|
581
|
+
)
|
|
582
|
+
});
|
|
259
583
|
}
|
|
260
|
-
const contentElement = Layout ? createElement(Layout, {}, pageElement) : pageElement;
|
|
261
584
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
262
|
-
const metadata =
|
|
263
|
-
params: {},
|
|
264
|
-
query: {},
|
|
265
|
-
props,
|
|
266
|
-
appConfig: this.appConfig
|
|
267
|
-
}) : this.appConfig.defaultMetadata;
|
|
585
|
+
const metadata = await this.resolveViewMetadata(view, props);
|
|
268
586
|
await this.prepareViewDependencies(view, Layout);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
587
|
+
await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
|
|
588
|
+
const viewRender = await this.renderComponentWithForeignChildren({
|
|
589
|
+
component: view,
|
|
590
|
+
props: normalizedProps
|
|
591
|
+
});
|
|
592
|
+
const layoutRender = Layout ? await this.renderComponentWithForeignChildren({
|
|
593
|
+
component: Layout,
|
|
594
|
+
props: {},
|
|
595
|
+
children: viewRender.html
|
|
596
|
+
}) : void 0;
|
|
597
|
+
const documentRender = await this.renderComponentWithForeignChildren({
|
|
598
|
+
component: HtmlTemplate,
|
|
599
|
+
props: {
|
|
600
|
+
metadata,
|
|
601
|
+
pageProps: normalizedProps
|
|
602
|
+
},
|
|
603
|
+
children: layoutRender?.html ?? viewRender.html
|
|
604
|
+
});
|
|
605
|
+
this.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
|
|
606
|
+
const transformedHtml = await this.finalizeResolvedHtml({
|
|
607
|
+
html: `${this.DOC_TYPE}${documentRender.html}`,
|
|
608
|
+
partial: false,
|
|
609
|
+
documentAttributes: this.getRouterDocumentAttributes(),
|
|
610
|
+
htmlContributions: this.buildNonReactDocumentContributions(HtmlTemplate, normalizedProps)
|
|
611
|
+
});
|
|
612
|
+
return this.createHtmlResponse(transformedHtml, ctx);
|
|
293
613
|
} catch (error) {
|
|
294
614
|
throw this.createRenderError("Failed to render view", error);
|
|
295
615
|
}
|