@ecopages/react 0.2.0-alpha.8 → 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
package/src/react-renderer.js
CHANGED
|
@@ -1,19 +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
5
|
import { getAppBuildExecutor } from "@ecopages/core/build/build-adapter";
|
|
5
|
-
import { rapidhash } from "@ecopages/core/hash";
|
|
6
|
-
import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
|
|
7
6
|
import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
|
|
7
|
+
import { createRequire } from "node:module";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import {
|
|
10
|
-
import { renderToReadableStream, renderToString } from "react-dom/server";
|
|
11
|
-
import { PLUGIN_NAME } from "./react.plugin.js";
|
|
9
|
+
import { REACT_PLUGIN_NAME } from "./react.constants.js";
|
|
12
10
|
import { hasSingleRootElement } from "./utils/html-boundary.js";
|
|
13
11
|
import { ReactBundleService } from "./services/react-bundle.service.js";
|
|
14
12
|
import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
|
|
13
|
+
import { ReactMdxConfigDependencyService } from "./services/react-mdx-config-dependency.service.js";
|
|
15
14
|
import { ReactPageModuleService } from "./services/react-page-module.service.js";
|
|
16
|
-
import {
|
|
15
|
+
import { ReactPagePayloadService } from "./services/react-page-payload.service.js";
|
|
16
|
+
import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
|
|
17
17
|
class ReactRenderError extends Error {
|
|
18
18
|
constructor(message) {
|
|
19
19
|
super(message);
|
|
@@ -29,31 +29,44 @@ class BundleError extends Error {
|
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
31
|
class ReactRenderer extends IntegrationRenderer {
|
|
32
|
-
name =
|
|
32
|
+
name = REACT_PLUGIN_NAME;
|
|
33
33
|
componentDirectory = RESOLVED_ASSETS_DIR;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
reactRuntimeModules;
|
|
35
|
+
routerAdapter;
|
|
36
|
+
mdxCompilerOptions;
|
|
37
|
+
mdxExtensions;
|
|
38
|
+
hmrPageMetadataCache;
|
|
38
39
|
/**
|
|
39
40
|
* Enables explicit graph behavior for React page-entry bundling.
|
|
40
41
|
*
|
|
41
42
|
* When true, page-entry bundles disable AST server-only stripping and rely
|
|
42
43
|
* on explicit dependency declarations for browser graph composition.
|
|
43
44
|
*/
|
|
44
|
-
|
|
45
|
+
explicitGraphEnabled;
|
|
45
46
|
/** @internal */
|
|
46
47
|
bundleService;
|
|
47
48
|
/** @internal */
|
|
48
49
|
pageModuleService;
|
|
49
50
|
/** @internal */
|
|
50
51
|
hydrationAssetService;
|
|
52
|
+
/** @internal */
|
|
53
|
+
pagePayloadService;
|
|
54
|
+
/** @internal */
|
|
55
|
+
mdxConfigDependencyService;
|
|
51
56
|
constructor(options) {
|
|
52
|
-
|
|
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;
|
|
53
64
|
this.bundleService = new ReactBundleService({
|
|
54
65
|
rootDir: this.appConfig.rootDir,
|
|
55
|
-
routerAdapter:
|
|
56
|
-
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)
|
|
57
70
|
});
|
|
58
71
|
this.pageModuleService = new ReactPageModuleService({
|
|
59
72
|
rootDir: this.appConfig.rootDir,
|
|
@@ -62,17 +75,23 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
62
75
|
buildExecutor: getAppBuildExecutor(this.appConfig),
|
|
63
76
|
layoutsDir: this.appConfig.absolutePaths.layoutsDir,
|
|
64
77
|
componentsDir: this.appConfig.absolutePaths.componentsDir,
|
|
65
|
-
mdxCompilerOptions:
|
|
66
|
-
mdxExtensions:
|
|
78
|
+
mdxCompilerOptions: this.mdxCompilerOptions,
|
|
79
|
+
mdxExtensions: this.mdxExtensions,
|
|
67
80
|
integrationName: this.name,
|
|
68
|
-
hasRouterAdapter: Boolean(
|
|
81
|
+
hasRouterAdapter: Boolean(this.routerAdapter)
|
|
69
82
|
});
|
|
70
83
|
this.hydrationAssetService = new ReactHydrationAssetService({
|
|
71
84
|
srcDir: this.appConfig.srcDir,
|
|
72
|
-
routerAdapter:
|
|
85
|
+
routerAdapter: this.routerAdapter,
|
|
73
86
|
assetProcessingService: this.assetProcessingService,
|
|
74
87
|
bundleService: this.bundleService,
|
|
75
|
-
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
|
|
76
95
|
});
|
|
77
96
|
}
|
|
78
97
|
shouldRenderPageComponent() {
|
|
@@ -99,19 +118,11 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
99
118
|
const integration = this.getComponentIntegration(component);
|
|
100
119
|
return integration === void 0 || integration === this.name;
|
|
101
120
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
*
|
|
105
|
-
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
106
|
-
* page-data contract as fully React-owned documents so navigation and hydration
|
|
107
|
-
* can read one marker consistently.
|
|
108
|
-
*/
|
|
109
|
-
buildRouterPageDataScript(pageProps) {
|
|
110
|
-
const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
|
|
111
|
-
return `<script id="__ECO_PAGE_DATA__" type="application/json">${safeJson}<\/script>`;
|
|
121
|
+
getComponentRequires(component) {
|
|
122
|
+
return component?.requires;
|
|
112
123
|
}
|
|
113
124
|
getRouterDocumentAttributes() {
|
|
114
|
-
if (!
|
|
125
|
+
if (!this.routerAdapter) {
|
|
115
126
|
return void 0;
|
|
116
127
|
}
|
|
117
128
|
return {
|
|
@@ -139,20 +150,25 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
139
150
|
asNonReactShellComponent(component) {
|
|
140
151
|
return component;
|
|
141
152
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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;
|
|
156
172
|
}
|
|
157
173
|
/**
|
|
158
174
|
* Appends route hydration assets for a concrete page/view file to the current
|
|
@@ -162,26 +178,8 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
162
178
|
if (!filePath) {
|
|
163
179
|
return;
|
|
164
180
|
}
|
|
165
|
-
const
|
|
166
|
-
this.
|
|
167
|
-
...this.htmlTransformer.getProcessedDependencies(),
|
|
168
|
-
...hydrationAssets
|
|
169
|
-
]);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Resolves metadata for direct `renderToResponse()` calls.
|
|
173
|
-
*
|
|
174
|
-
* View rendering bypasses the normal route-file pipeline, so metadata has to be
|
|
175
|
-
* evaluated here from either the component-level generator or the application
|
|
176
|
-
* default.
|
|
177
|
-
*/
|
|
178
|
-
async resolveViewMetadata(view, props) {
|
|
179
|
-
return view.metadata ? await view.metadata({
|
|
180
|
-
params: {},
|
|
181
|
-
query: {},
|
|
182
|
-
props,
|
|
183
|
-
appConfig: this.appConfig
|
|
184
|
-
}) : this.appConfig.defaultMetadata;
|
|
181
|
+
const pageBrowserGraph = await this.resolvePageBrowserGraphForFile(filePath);
|
|
182
|
+
this.mergePageBrowserGraphIntoPagePackage(pageBrowserGraph);
|
|
185
183
|
}
|
|
186
184
|
/**
|
|
187
185
|
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
@@ -198,110 +196,242 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
198
196
|
throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
|
|
199
197
|
}
|
|
200
198
|
/**
|
|
201
|
-
*
|
|
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.
|
|
202
206
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
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.
|
|
206
210
|
*/
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
);
|
|
213
217
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
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;
|
|
220
223
|
}
|
|
221
|
-
const
|
|
222
|
-
this.
|
|
223
|
-
{ ...layoutProps, children: pageHtml },
|
|
224
|
-
"Layout"
|
|
224
|
+
const html = reactDomServer.renderToString(
|
|
225
|
+
react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
225
226
|
);
|
|
226
|
-
return
|
|
227
|
+
return this.normalizeUnresolvedMarkerArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
227
228
|
}
|
|
228
229
|
/**
|
|
229
|
-
*
|
|
230
|
+
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
230
231
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
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.
|
|
234
237
|
*/
|
|
235
|
-
|
|
236
|
-
if (
|
|
237
|
-
return
|
|
238
|
-
createElement(
|
|
239
|
-
this.asReactComponent(options.HtmlTemplate),
|
|
240
|
-
{
|
|
241
|
-
metadata: options.metadata,
|
|
242
|
-
pageProps: options.pageProps
|
|
243
|
-
},
|
|
244
|
-
options.contentNode
|
|
245
|
-
)
|
|
246
|
-
);
|
|
238
|
+
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
239
|
+
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
240
|
+
return html;
|
|
247
241
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
258
265
|
);
|
|
266
|
+
return html;
|
|
259
267
|
}
|
|
260
268
|
/**
|
|
261
|
-
*
|
|
269
|
+
* Resolves queued renderer-owned foreign-subtree tokens produced during React component rendering.
|
|
262
270
|
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
* -
|
|
266
|
-
*
|
|
267
|
-
|
|
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.
|
|
268
323
|
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
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.
|
|
271
327
|
*/
|
|
272
|
-
async
|
|
273
|
-
|
|
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) {
|
|
274
365
|
const componentConfig = input.component.config;
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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);
|
|
279
375
|
const componentFile = componentConfig?.__eco?.file;
|
|
280
|
-
const context = input.integrationContext ?? {};
|
|
281
376
|
let rootAttributes;
|
|
282
377
|
let assets;
|
|
283
|
-
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService) {
|
|
378
|
+
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService && !hasResolvedChildHtml) {
|
|
284
379
|
const componentInstanceId = context.componentInstanceId;
|
|
285
|
-
assets = await this.hydrationAssetService.buildComponentRenderAssets(
|
|
286
|
-
componentFile,
|
|
287
|
-
componentInstanceId,
|
|
288
|
-
input.props,
|
|
289
|
-
componentConfig
|
|
290
|
-
);
|
|
380
|
+
assets = await this.hydrationAssetService.buildComponentRenderAssets(componentFile, componentConfig);
|
|
291
381
|
rootAttributes = {
|
|
292
382
|
"data-eco-component-id": componentInstanceId,
|
|
293
|
-
"data-eco-
|
|
383
|
+
"data-eco-component-key": getReactIslandComponentKey(componentFile, componentConfig),
|
|
384
|
+
"data-eco-props": btoa(JSON.stringify(this.buildHydrationProps(input.props)))
|
|
294
385
|
};
|
|
295
386
|
}
|
|
387
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
388
|
+
...assets ?? [],
|
|
389
|
+
...queuedForeignSubtreeResolution.assets
|
|
390
|
+
]);
|
|
296
391
|
return {
|
|
297
392
|
html,
|
|
298
393
|
canAttachAttributes,
|
|
299
394
|
rootTag,
|
|
300
395
|
integrationName: this.name,
|
|
301
396
|
rootAttributes,
|
|
302
|
-
assets
|
|
397
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
303
398
|
};
|
|
304
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
|
+
}
|
|
305
435
|
/**
|
|
306
436
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
307
437
|
* @param filePath - The file path to check
|
|
@@ -310,126 +440,49 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
310
440
|
isMdxFile(filePath) {
|
|
311
441
|
return this.pageModuleService.isMdxFile(filePath);
|
|
312
442
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
* @param pagePath - Absolute path to the MDX page file
|
|
316
|
-
* @returns Processed assets for MDX configuration dependencies
|
|
317
|
-
*/
|
|
318
|
-
async processMdxConfigDependencies(pagePath) {
|
|
319
|
-
const { config } = await this.importPageFile(pagePath);
|
|
320
|
-
const resolvedLayout = config?.layout;
|
|
321
|
-
const components = [];
|
|
322
|
-
if (resolvedLayout?.config?.dependencies) {
|
|
323
|
-
const layoutConfig = this.pageModuleService.ensureConfigFileMetadata(resolvedLayout.config, pagePath);
|
|
324
|
-
components.push({ config: layoutConfig });
|
|
325
|
-
}
|
|
326
|
-
if (config?.dependencies) {
|
|
327
|
-
const configWithMeta = {
|
|
328
|
-
...config,
|
|
329
|
-
__eco: { id: rapidhash(pagePath).toString(36), file: pagePath, integration: "react" }
|
|
330
|
-
};
|
|
331
|
-
components.push({ config: configWithMeta });
|
|
332
|
-
}
|
|
333
|
-
const processedDependencies = await this.processComponentDependencies(components);
|
|
334
|
-
const eagerSsrLazyDependencies = await this.processDeclaredMdxSsrLazyDependencies(components, pagePath);
|
|
335
|
-
return [...processedDependencies, ...eagerSsrLazyDependencies];
|
|
443
|
+
usesIntegrationPageImporter(file) {
|
|
444
|
+
return this.pageModuleService.isMdxFile(file);
|
|
336
445
|
}
|
|
337
|
-
async
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
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;
|
|
344
454
|
}
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
351
|
-
const normalizeAttributes = (attributes) => ({
|
|
352
|
-
type: "module",
|
|
353
|
-
defer: "",
|
|
354
|
-
...attributes ?? {}
|
|
355
|
-
});
|
|
356
|
-
const collect = (config) => {
|
|
357
|
-
if (!config || visitedConfigs.has(config)) {
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
visitedConfigs.add(config);
|
|
361
|
-
const componentFile = config.__eco?.file;
|
|
362
|
-
if (componentFile) {
|
|
363
|
-
const componentDir = path.dirname(componentFile);
|
|
364
|
-
for (const script of config.dependencies?.scripts ?? []) {
|
|
365
|
-
if (typeof script === "string" || !script.lazy || script.ssr !== true) {
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
const attributes = normalizeAttributes(script.attributes);
|
|
369
|
-
if (script.content) {
|
|
370
|
-
const key2 = `content:${script.content}:${JSON.stringify(attributes)}`;
|
|
371
|
-
if (seenKeys.has(key2)) {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
seenKeys.add(key2);
|
|
375
|
-
dependencies.push(
|
|
376
|
-
AssetFactory.createContentScript({
|
|
377
|
-
position: "head",
|
|
378
|
-
content: script.content,
|
|
379
|
-
attributes
|
|
380
|
-
})
|
|
381
|
-
);
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
if (!script.src) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const resolvedPath = path.resolve(componentDir, script.src);
|
|
388
|
-
const key = `file:${resolvedPath}:${JSON.stringify(attributes)}`;
|
|
389
|
-
if (seenKeys.has(key)) {
|
|
390
|
-
continue;
|
|
391
|
-
}
|
|
392
|
-
seenKeys.add(key);
|
|
393
|
-
dependencies.push(
|
|
394
|
-
AssetFactory.createFileScript({
|
|
395
|
-
filepath: resolvedPath,
|
|
396
|
-
position: "head",
|
|
397
|
-
attributes
|
|
398
|
-
})
|
|
399
|
-
);
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
if (config.layout?.config) {
|
|
403
|
-
collect(config.layout.config);
|
|
404
|
-
}
|
|
405
|
-
for (const nestedComponent of config.dependencies?.components ?? []) {
|
|
406
|
-
collect(nestedComponent?.config);
|
|
407
|
-
}
|
|
455
|
+
return {
|
|
456
|
+
...pageModule,
|
|
457
|
+
default: Page,
|
|
458
|
+
getMetadata,
|
|
459
|
+
config
|
|
408
460
|
};
|
|
409
|
-
for (const component of components) {
|
|
410
|
-
collect(component.config);
|
|
411
|
-
}
|
|
412
|
-
return dependencies;
|
|
413
461
|
}
|
|
414
|
-
async
|
|
462
|
+
async collectPageBrowserGraphContribution(context) {
|
|
415
463
|
try {
|
|
416
|
-
const pageModule =
|
|
417
|
-
const shouldHydrate =
|
|
464
|
+
const { file: pagePath, pageModule } = context;
|
|
465
|
+
const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
|
|
418
466
|
if (!shouldHydrate) {
|
|
419
|
-
return [];
|
|
467
|
+
return { assets: [] };
|
|
420
468
|
}
|
|
421
469
|
const isMdx = this.pageModuleService.isMdxFile(pagePath);
|
|
422
470
|
const declaredModules = this.pageModuleService.collectPageDeclaredModules(pageModule);
|
|
423
|
-
const
|
|
471
|
+
const dependencies = await this.hydrationAssetService.createPageBrowserGraphDependencies(
|
|
424
472
|
pagePath,
|
|
425
473
|
isMdx,
|
|
426
474
|
declaredModules
|
|
427
475
|
);
|
|
476
|
+
const assets = [];
|
|
428
477
|
if (isMdx) {
|
|
429
|
-
const mdxConfigAssets = await this.processMdxConfigDependencies(
|
|
430
|
-
|
|
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);
|
|
431
484
|
}
|
|
432
|
-
return
|
|
485
|
+
return { dependencies, assets };
|
|
433
486
|
} catch (error) {
|
|
434
487
|
if (error instanceof BundleError) {
|
|
435
488
|
console.error("[ecopages] Bundle errors:", error.logs);
|
|
@@ -439,26 +492,6 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
439
492
|
);
|
|
440
493
|
}
|
|
441
494
|
}
|
|
442
|
-
/**
|
|
443
|
-
* Imports a page module while normalizing React MDX modules to the same shape
|
|
444
|
-
* as ordinary React page files.
|
|
445
|
-
*
|
|
446
|
-
* MDX page imports can expose `config` separately from the default export. The
|
|
447
|
-
* React renderer reattaches that config to the page component so downstream
|
|
448
|
-
* layout, dependency, and hydration logic can treat MDX and TSX pages the same.
|
|
449
|
-
*/
|
|
450
|
-
async importPageFile(file) {
|
|
451
|
-
const module = this.pageModuleService.isMdxFile(file) ? await this.pageModuleService.importMdxPageFile(file) : await super.importPageFile(file);
|
|
452
|
-
const { default: Page, getMetadata, config } = module;
|
|
453
|
-
if (this.pageModuleService.isMdxFile(file) && config) {
|
|
454
|
-
Page.config = config;
|
|
455
|
-
}
|
|
456
|
-
return {
|
|
457
|
-
default: Page,
|
|
458
|
-
getMetadata,
|
|
459
|
-
config
|
|
460
|
-
};
|
|
461
|
-
}
|
|
462
495
|
/**
|
|
463
496
|
* Renders a full route response for the filesystem page pipeline.
|
|
464
497
|
*
|
|
@@ -480,71 +513,48 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
480
513
|
pageProps
|
|
481
514
|
}) {
|
|
482
515
|
try {
|
|
483
|
-
const safeLocals = this.getSerializableLocals(locals, Page
|
|
484
|
-
const allPageProps = this.buildSerializedPageProps({
|
|
516
|
+
const safeLocals = this.pagePayloadService.getSerializableLocals(locals, this.getComponentRequires(Page));
|
|
517
|
+
const allPageProps = this.pagePayloadService.buildSerializedPageProps({
|
|
485
518
|
pageProps,
|
|
486
519
|
params,
|
|
487
520
|
query,
|
|
488
521
|
safeLocals
|
|
489
522
|
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
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,
|
|
498
533
|
metadata,
|
|
499
|
-
pageProps: allPageProps
|
|
500
|
-
contentNode,
|
|
501
|
-
contentHtml
|
|
534
|
+
pageProps: allPageProps
|
|
502
535
|
});
|
|
503
536
|
} catch (error) {
|
|
504
537
|
throw this.createRenderError("Failed to render component", error);
|
|
505
538
|
}
|
|
506
539
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
}
|
|
510
|
-
/**
|
|
511
|
-
* Safely extracts the declared subset of locals for client-side hydration.
|
|
512
|
-
*
|
|
513
|
-
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
514
|
-
* request-scoped data (e.g., session). Only keys explicitly declared via
|
|
515
|
-
* `Page.requires` are serialized to the client so sensitive request-only data
|
|
516
|
-
* is not leaked into hydration payloads by default.
|
|
517
|
-
*
|
|
518
|
-
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
519
|
-
* to prevent accidental use. This method safely detects that case and returns
|
|
520
|
-
* `undefined` instead of throwing.
|
|
521
|
-
*
|
|
522
|
-
* @param locals - The locals object from the render context
|
|
523
|
-
* @param requiredLocals - Keys explicitly requested for client hydration
|
|
524
|
-
* @returns The filtered locals object if serializable, undefined otherwise
|
|
525
|
-
*/
|
|
526
|
-
getSerializableLocals(locals, requiredLocals) {
|
|
527
|
-
try {
|
|
528
|
-
if (!locals) {
|
|
529
|
-
return void 0;
|
|
530
|
-
}
|
|
531
|
-
const requiredKeys = requiredLocals ? Array.isArray(requiredLocals) ? requiredLocals : [requiredLocals] : [];
|
|
532
|
-
if (requiredKeys.length === 0) {
|
|
533
|
-
return void 0;
|
|
534
|
-
}
|
|
535
|
-
const serializedLocals = Object.fromEntries(
|
|
536
|
-
requiredKeys.filter((key) => Object.prototype.hasOwnProperty.call(locals, key)).map((key) => [key, locals[key]])
|
|
537
|
-
);
|
|
538
|
-
if (Object.keys(serializedLocals).length > 0) {
|
|
539
|
-
return serializedLocals;
|
|
540
|
-
}
|
|
540
|
+
getHtmlDocumentContributions(options) {
|
|
541
|
+
if (options.partial || !options.renderOptions) {
|
|
541
542
|
return void 0;
|
|
542
|
-
} catch (e) {
|
|
543
|
-
if (e instanceof LocalsAccessError) {
|
|
544
|
-
return void 0;
|
|
545
|
-
}
|
|
546
|
-
throw e;
|
|
547
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();
|
|
548
558
|
}
|
|
549
559
|
/**
|
|
550
560
|
* Renders an arbitrary React view through the application's HTML shell.
|
|
@@ -556,43 +566,49 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
556
566
|
*/
|
|
557
567
|
async renderToResponse(view, props, ctx) {
|
|
558
568
|
try {
|
|
569
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
559
570
|
const viewConfig = view.config;
|
|
560
571
|
const Layout = viewConfig?.layout;
|
|
561
572
|
const ViewComponent = this.asReactComponent(view);
|
|
562
573
|
const normalizedProps = props ?? {};
|
|
563
574
|
if (ctx.partial) {
|
|
564
|
-
|
|
565
|
-
|
|
575
|
+
return this.renderPartialViewResponse({
|
|
576
|
+
view,
|
|
577
|
+
props,
|
|
578
|
+
ctx,
|
|
579
|
+
renderInline: async () => await reactDomServer.renderToReadableStream(
|
|
580
|
+
react.createElement(ViewComponent, normalizedProps)
|
|
581
|
+
)
|
|
582
|
+
});
|
|
566
583
|
}
|
|
567
584
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
568
585
|
const metadata = await this.resolveViewMetadata(view, props);
|
|
569
586
|
await this.prepareViewDependencies(view, Layout);
|
|
570
587
|
await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
pageProps: normalizedProps
|
|
588
|
+
const viewRender = await this.renderComponentWithForeignChildren({
|
|
589
|
+
component: view,
|
|
590
|
+
props: normalizedProps
|
|
575
591
|
});
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
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)
|
|
582
611
|
});
|
|
583
|
-
const transformedResponse = await this.htmlTransformer.transform(
|
|
584
|
-
new Response(body, {
|
|
585
|
-
headers: { "Content-Type": "text/html" }
|
|
586
|
-
})
|
|
587
|
-
);
|
|
588
|
-
let transformedHtml = await transformedResponse.text();
|
|
589
|
-
const documentAttributes = this.getRouterDocumentAttributes();
|
|
590
|
-
if (documentAttributes) {
|
|
591
|
-
transformedHtml = this.htmlTransformer.applyAttributesToHtmlElement(
|
|
592
|
-
transformedHtml,
|
|
593
|
-
documentAttributes
|
|
594
|
-
);
|
|
595
|
-
}
|
|
596
612
|
return this.createHtmlResponse(transformedHtml, ctx);
|
|
597
613
|
} catch (error) {
|
|
598
614
|
throw this.createRenderError("Failed to render view", error);
|