@ecopages/react 0.2.0-alpha.14 → 0.2.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/package.json +3 -3
- package/src/react-renderer.d.ts +75 -58
- package/src/react-renderer.js +181 -240
- package/src/react.plugin.d.ts +20 -91
- package/src/react.plugin.js +85 -35
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- 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 +5 -2
- package/src/services/react-page-module.service.js +21 -21
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- 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 +3 -15
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +126 -0
package/src/react-renderer.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
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 { createElement, Fragment } from "react";
|
|
10
|
-
import { renderToReadableStream, renderToString } from "react-dom/server";
|
|
11
9
|
import { PLUGIN_NAME } from "./react.plugin.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";
|
|
15
|
+
import { ReactPagePayloadService } from "./services/react-page-payload.service.js";
|
|
16
16
|
import { getReactIslandComponentKey, ReactHydrationAssetService } from "./services/react-hydration-asset.service.js";
|
|
17
17
|
class ReactRenderError extends Error {
|
|
18
18
|
constructor(message) {
|
|
@@ -31,29 +31,40 @@ class BundleError extends Error {
|
|
|
31
31
|
class ReactRenderer extends IntegrationRenderer {
|
|
32
32
|
name = 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,
|
|
57
68
|
jsxImportSource: (this.appConfig.integrations ?? []).find((integration) => integration.name === this.name)?.jsxImportSource,
|
|
58
69
|
nonReactExtensions: (this.appConfig.integrations ?? []).filter((integration) => integration.name !== this.name).flatMap((integration) => integration.extensions)
|
|
59
70
|
});
|
|
@@ -64,17 +75,23 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
64
75
|
buildExecutor: getAppBuildExecutor(this.appConfig),
|
|
65
76
|
layoutsDir: this.appConfig.absolutePaths.layoutsDir,
|
|
66
77
|
componentsDir: this.appConfig.absolutePaths.componentsDir,
|
|
67
|
-
mdxCompilerOptions:
|
|
68
|
-
mdxExtensions:
|
|
78
|
+
mdxCompilerOptions: this.mdxCompilerOptions,
|
|
79
|
+
mdxExtensions: this.mdxExtensions,
|
|
69
80
|
integrationName: this.name,
|
|
70
|
-
hasRouterAdapter: Boolean(
|
|
81
|
+
hasRouterAdapter: Boolean(this.routerAdapter)
|
|
71
82
|
});
|
|
72
83
|
this.hydrationAssetService = new ReactHydrationAssetService({
|
|
73
84
|
srcDir: this.appConfig.srcDir,
|
|
74
|
-
routerAdapter:
|
|
85
|
+
routerAdapter: this.routerAdapter,
|
|
75
86
|
assetProcessingService: this.assetProcessingService,
|
|
76
87
|
bundleService: this.bundleService,
|
|
77
|
-
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
|
|
78
95
|
});
|
|
79
96
|
}
|
|
80
97
|
shouldRenderPageComponent() {
|
|
@@ -101,19 +118,8 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
101
118
|
const integration = this.getComponentIntegration(component);
|
|
102
119
|
return integration === void 0 || integration === this.name;
|
|
103
120
|
}
|
|
104
|
-
/**
|
|
105
|
-
* Creates the canonical page-props payload used by router hydration.
|
|
106
|
-
*
|
|
107
|
-
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
108
|
-
* page-data contract as fully React-owned documents so navigation and hydration
|
|
109
|
-
* can read one shared document payload consistently.
|
|
110
|
-
*/
|
|
111
|
-
buildRouterPageDataScript(pageProps) {
|
|
112
|
-
const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
|
|
113
|
-
return `<script id="__ECO_PAGE_DATA__" type="application/json">${safeJson}<\/script>`;
|
|
114
|
-
}
|
|
115
121
|
getRouterDocumentAttributes() {
|
|
116
|
-
if (!
|
|
122
|
+
if (!this.routerAdapter) {
|
|
117
123
|
return void 0;
|
|
118
124
|
}
|
|
119
125
|
return {
|
|
@@ -141,20 +147,25 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
141
147
|
asNonReactShellComponent(component) {
|
|
142
148
|
return component;
|
|
143
149
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
150
|
+
resolveReactRuntimeModules() {
|
|
151
|
+
const appPackageJsonPath = path.resolve(this.appConfig.rootDir || process.cwd(), "package.json");
|
|
152
|
+
try {
|
|
153
|
+
const requireFromApp = createRequire(appPackageJsonPath);
|
|
154
|
+
return {
|
|
155
|
+
react: requireFromApp("react"),
|
|
156
|
+
reactDomServer: requireFromApp("react-dom/server")
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
const requireFromIntegration = createRequire(import.meta.url);
|
|
160
|
+
return {
|
|
161
|
+
react: requireFromIntegration("react"),
|
|
162
|
+
reactDomServer: requireFromIntegration("react-dom/server")
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
getReactRuntimeModules() {
|
|
167
|
+
this.reactRuntimeModules ??= this.resolveReactRuntimeModules();
|
|
168
|
+
return this.reactRuntimeModules;
|
|
158
169
|
}
|
|
159
170
|
/**
|
|
160
171
|
* Appends route hydration assets for a concrete page/view file to the current
|
|
@@ -195,9 +206,10 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
195
206
|
* @returns Serialized component HTML with resolved child markup preserved.
|
|
196
207
|
*/
|
|
197
208
|
renderComponentHtml(input, context, runtimeContext) {
|
|
209
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
198
210
|
if (input.children === void 0) {
|
|
199
211
|
return this.normalizeBoundaryArtifactHtml(
|
|
200
|
-
renderToString(createElement(this.asReactComponent(input.component), input.props))
|
|
212
|
+
reactDomServer.renderToString(react.createElement(this.asReactComponent(input.component), input.props))
|
|
201
213
|
);
|
|
202
214
|
}
|
|
203
215
|
const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
|
|
@@ -206,28 +218,54 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
206
218
|
runtimeContext.rawChildrenToken = rawChildrenToken;
|
|
207
219
|
runtimeContext.rawChildrenHtml = resolvedChildHtml;
|
|
208
220
|
}
|
|
209
|
-
const html = renderToString(
|
|
210
|
-
createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
221
|
+
const html = reactDomServer.renderToString(
|
|
222
|
+
react.createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
211
223
|
);
|
|
212
224
|
return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
213
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Restores raw child HTML that was temporarily replaced by a token during React SSR.
|
|
228
|
+
*
|
|
229
|
+
* Queued boundary resolution may render children through a fragment path before all
|
|
230
|
+
* nested integration tokens are resolved. When that happens, React must never see
|
|
231
|
+
* the resolved child HTML as a normal string child or it would escape it. The
|
|
232
|
+
* runtime context stores the placeholder token and the raw child HTML so the
|
|
233
|
+
* fragment render path can reinsert it before foreign boundary tokens are handled.
|
|
234
|
+
*/
|
|
214
235
|
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
215
236
|
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
216
237
|
return html;
|
|
217
238
|
}
|
|
218
239
|
return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
|
|
219
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Renders queued child content through React and then resolves nested boundary tokens.
|
|
243
|
+
*
|
|
244
|
+
* This path is only used for children that were deferred while React rendered the
|
|
245
|
+
* parent boundary. It first restores any raw child HTML placeholders owned by the
|
|
246
|
+
* current runtime context, then asks the shared queued-boundary resolver to swap
|
|
247
|
+
* foreign integration tokens with their resolved HTML.
|
|
248
|
+
*/
|
|
220
249
|
async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
|
|
221
250
|
if (children === void 0) {
|
|
222
251
|
return void 0;
|
|
223
252
|
}
|
|
253
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
224
254
|
let html = this.normalizeBoundaryArtifactHtml(
|
|
225
|
-
renderToString(createElement(Fragment, null, children))
|
|
255
|
+
reactDomServer.renderToString(react.createElement(react.Fragment, null, children))
|
|
226
256
|
);
|
|
227
257
|
html = this.restoreRuntimeChildHtml(html, runtimeContext);
|
|
228
258
|
html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
|
|
229
259
|
return html;
|
|
230
260
|
}
|
|
261
|
+
/**
|
|
262
|
+
* Resolves queued renderer-owned boundary tokens produced during React component rendering.
|
|
263
|
+
*
|
|
264
|
+
* React components can enqueue nested boundaries while the parent HTML is being
|
|
265
|
+
* rendered. This delegates to the shared renderer-owned queue resolver but keeps
|
|
266
|
+
* the React-specific child rendering behavior local so raw child HTML and React's
|
|
267
|
+
* fragment rendering semantics stay coordinated.
|
|
268
|
+
*/
|
|
231
269
|
async resolveQueuedBoundaryHtml(html, runtimeContext) {
|
|
232
270
|
return this.resolveRendererOwnedQueuedBoundaryHtml({
|
|
233
271
|
html,
|
|
@@ -255,58 +293,71 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
255
293
|
return hydrationProps;
|
|
256
294
|
}
|
|
257
295
|
/**
|
|
258
|
-
*
|
|
296
|
+
* Builds the extra document props needed when React renders through a non-React HTML shell.
|
|
259
297
|
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
298
|
+
* Router-backed React pages still need to publish the canonical page-data script
|
|
299
|
+
* even when the outer document shell belongs to another integration.
|
|
300
|
+
*/
|
|
301
|
+
buildNonReactDocumentProps(htmlTemplate, pageProps) {
|
|
302
|
+
if (this.isReactManagedComponent(htmlTemplate) || !this.routerAdapter) {
|
|
303
|
+
return void 0;
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
headContent: this.pagePayloadService.buildRouterPageDataScript(pageProps)
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* Renders a foreign integration component boundary that participates in React composition.
|
|
267
311
|
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
312
|
+
* Non-React components must resolve to serialized HTML so React can embed them as
|
|
313
|
+
* mixed-shell boundaries. Any component-owned dependencies still need to flow
|
|
314
|
+
* through the shared dependency resolver before queued boundary tokens are finalized.
|
|
270
315
|
*/
|
|
271
|
-
async
|
|
272
|
-
|
|
273
|
-
if (
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
...input.props,
|
|
278
|
-
children: typeof input.children === "string" ? input.children : String(input.children ?? "")
|
|
279
|
-
};
|
|
280
|
-
}
|
|
281
|
-
const html2 = await this.renderNonReactShellComponent(
|
|
282
|
-
this.asNonReactShellComponent(input.component),
|
|
283
|
-
props,
|
|
284
|
-
"Component"
|
|
285
|
-
);
|
|
286
|
-
const hasDependencies = Boolean(input.component.config?.dependencies);
|
|
287
|
-
const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
|
|
288
|
-
const assets2 = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
|
|
289
|
-
const queuedBoundaryResolution2 = await this.resolveQueuedBoundaryHtml(html2, runtimeContext);
|
|
290
|
-
const mergedAssets2 = this.htmlTransformer.dedupeProcessedAssets([
|
|
291
|
-
...assets2 ?? [],
|
|
292
|
-
...queuedBoundaryResolution2.assets
|
|
293
|
-
]);
|
|
294
|
-
return {
|
|
295
|
-
html: queuedBoundaryResolution2.html,
|
|
296
|
-
canAttachAttributes: true,
|
|
297
|
-
rootTag: this.getRootTagName(queuedBoundaryResolution2.html),
|
|
298
|
-
integrationName: this.name,
|
|
299
|
-
assets: mergedAssets2.length > 0 ? mergedAssets2 : void 0
|
|
316
|
+
async renderForeignComponentBoundary(input, runtimeContext) {
|
|
317
|
+
let props = input.props;
|
|
318
|
+
if (input.children !== void 0) {
|
|
319
|
+
props = {
|
|
320
|
+
...input.props,
|
|
321
|
+
children: typeof input.children === "string" ? input.children : String(input.children ?? "")
|
|
300
322
|
};
|
|
301
323
|
}
|
|
324
|
+
const html = await this.renderNonReactShellComponent(
|
|
325
|
+
this.asNonReactShellComponent(input.component),
|
|
326
|
+
props,
|
|
327
|
+
"Component"
|
|
328
|
+
);
|
|
329
|
+
const hasDependencies = Boolean(input.component.config?.dependencies);
|
|
330
|
+
const canResolveAssets = typeof this.assetProcessingService?.processDependencies === "function";
|
|
331
|
+
const assets = hasDependencies && canResolveAssets ? await this.processComponentDependencies([input.component]) : void 0;
|
|
332
|
+
const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
|
|
333
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
334
|
+
...assets ?? [],
|
|
335
|
+
...queuedBoundaryResolution.assets
|
|
336
|
+
]);
|
|
337
|
+
return {
|
|
338
|
+
html: queuedBoundaryResolution.html,
|
|
339
|
+
canAttachAttributes: true,
|
|
340
|
+
rootTag: this.getRootTagName(queuedBoundaryResolution.html),
|
|
341
|
+
integrationName: this.name,
|
|
342
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Renders a React-owned component boundary and attaches island hydration metadata when possible.
|
|
347
|
+
*
|
|
348
|
+
* This path keeps React-owned SSR, queued boundary resolution, and optional
|
|
349
|
+
* island hydration wiring together so the public `renderComponent()` method can
|
|
350
|
+
* read as orchestration rather than implementation detail.
|
|
351
|
+
*/
|
|
352
|
+
async renderReactComponentBoundary(input, runtimeContext) {
|
|
302
353
|
const componentConfig = input.component.config;
|
|
303
354
|
const context = input.integrationContext ?? {};
|
|
304
355
|
const hasResolvedChildHtml = input.children !== void 0;
|
|
305
356
|
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
306
357
|
const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
|
|
307
358
|
html = queuedBoundaryResolution.html;
|
|
308
|
-
|
|
309
|
-
|
|
359
|
+
const canAttachAttributes = hasSingleRootElement(html);
|
|
360
|
+
const rootTag = this.getRootTagName(html);
|
|
310
361
|
const componentFile = componentConfig?.__eco?.file;
|
|
311
362
|
let rootAttributes;
|
|
312
363
|
let assets;
|
|
@@ -332,6 +383,27 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
332
383
|
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
333
384
|
};
|
|
334
385
|
}
|
|
386
|
+
/**
|
|
387
|
+
* Renders a React component for component-level orchestration.
|
|
388
|
+
*
|
|
389
|
+
* Behavior:
|
|
390
|
+
* - SSR always returns the component's own root HTML (no synthetic wrapper).
|
|
391
|
+
* - When an explicit component instance id is provided, a stable
|
|
392
|
+
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
393
|
+
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
394
|
+
* - When resolved child HTML is provided, that boundary is treated as a pure SSR
|
|
395
|
+
* composition step and does not emit hydration assets for the parent wrapper.
|
|
396
|
+
*
|
|
397
|
+
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
398
|
+
* deterministic mount target per component instance.
|
|
399
|
+
*/
|
|
400
|
+
async renderComponent(input) {
|
|
401
|
+
const runtimeContext = this.getQueuedBoundaryRuntime(input);
|
|
402
|
+
if (!this.isReactManagedComponent(input.component)) {
|
|
403
|
+
return this.renderForeignComponentBoundary(input, runtimeContext);
|
|
404
|
+
}
|
|
405
|
+
return this.renderReactComponentBoundary(input, runtimeContext);
|
|
406
|
+
}
|
|
335
407
|
createComponentBoundaryRuntime(options) {
|
|
336
408
|
return this.createQueuedBoundaryRuntime({
|
|
337
409
|
boundaryInput: options.boundaryInput,
|
|
@@ -357,8 +429,8 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
357
429
|
usesIntegrationPageImporter(file) {
|
|
358
430
|
return this.pageModuleService.isMdxFile(file);
|
|
359
431
|
}
|
|
360
|
-
async importIntegrationPageFile(file) {
|
|
361
|
-
return await this.pageModuleService.importMdxPageFile(file);
|
|
432
|
+
async importIntegrationPageFile(file, options) {
|
|
433
|
+
return await this.pageModuleService.importMdxPageFile(file, options);
|
|
362
434
|
}
|
|
363
435
|
normalizeImportedPageFile(file, pageModule) {
|
|
364
436
|
const reactModule = pageModule;
|
|
@@ -373,112 +445,10 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
373
445
|
config
|
|
374
446
|
};
|
|
375
447
|
}
|
|
376
|
-
/**
|
|
377
|
-
* Processes MDX-specific configuration dependencies including layout dependencies.
|
|
378
|
-
* @param pagePath - Absolute path to the MDX page file
|
|
379
|
-
* @returns Processed assets for MDX configuration dependencies
|
|
380
|
-
*/
|
|
381
|
-
async processMdxConfigDependencies(pagePath) {
|
|
382
|
-
const pageModule = await this.importPageFile(pagePath);
|
|
383
|
-
const config = pageModule.config;
|
|
384
|
-
const resolvedLayout = config?.layout;
|
|
385
|
-
const components = [];
|
|
386
|
-
if (resolvedLayout?.config?.dependencies) {
|
|
387
|
-
const layoutConfig = this.pageModuleService.ensureConfigFileMetadata(resolvedLayout.config, pagePath);
|
|
388
|
-
components.push({ config: layoutConfig });
|
|
389
|
-
}
|
|
390
|
-
if (config?.dependencies) {
|
|
391
|
-
const configWithMeta = {
|
|
392
|
-
...config,
|
|
393
|
-
__eco: { id: rapidhash(pagePath).toString(36), file: pagePath, integration: "react" }
|
|
394
|
-
};
|
|
395
|
-
components.push({ config: configWithMeta });
|
|
396
|
-
}
|
|
397
|
-
const processedDependencies = await this.processComponentDependencies(components);
|
|
398
|
-
const eagerSsrLazyDependencies = await this.processDeclaredMdxSsrLazyDependencies(components, pagePath);
|
|
399
|
-
return [...processedDependencies, ...eagerSsrLazyDependencies];
|
|
400
|
-
}
|
|
401
|
-
async processDeclaredMdxSsrLazyDependencies(components, pagePath) {
|
|
402
|
-
if (!this.assetProcessingService?.processDependencies) {
|
|
403
|
-
return [];
|
|
404
|
-
}
|
|
405
|
-
const dependencies = this.collectDeclaredMdxSsrLazyDependencies(components);
|
|
406
|
-
if (dependencies.length === 0) {
|
|
407
|
-
return [];
|
|
408
|
-
}
|
|
409
|
-
return this.assetProcessingService.processDependencies(dependencies, `react-mdx-ssr-lazy:${pagePath}`);
|
|
410
|
-
}
|
|
411
|
-
collectDeclaredMdxSsrLazyDependencies(components) {
|
|
412
|
-
const dependencies = [];
|
|
413
|
-
const visitedConfigs = /* @__PURE__ */ new Set();
|
|
414
|
-
const seenKeys = /* @__PURE__ */ new Set();
|
|
415
|
-
const normalizeAttributes = (attributes) => ({
|
|
416
|
-
type: "module",
|
|
417
|
-
defer: "",
|
|
418
|
-
...attributes ?? {}
|
|
419
|
-
});
|
|
420
|
-
const collect = (config) => {
|
|
421
|
-
if (!config || visitedConfigs.has(config)) {
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
visitedConfigs.add(config);
|
|
425
|
-
const componentFile = config.__eco?.file;
|
|
426
|
-
if (componentFile) {
|
|
427
|
-
const componentDir = path.dirname(componentFile);
|
|
428
|
-
for (const script of config.dependencies?.scripts ?? []) {
|
|
429
|
-
if (typeof script === "string" || !script.lazy || script.ssr !== true) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
const attributes = normalizeAttributes(script.attributes);
|
|
433
|
-
if (script.content) {
|
|
434
|
-
const key2 = `content:${script.content}:${JSON.stringify(attributes)}`;
|
|
435
|
-
if (seenKeys.has(key2)) {
|
|
436
|
-
continue;
|
|
437
|
-
}
|
|
438
|
-
seenKeys.add(key2);
|
|
439
|
-
dependencies.push(
|
|
440
|
-
AssetFactory.createContentScript({
|
|
441
|
-
position: "head",
|
|
442
|
-
content: script.content,
|
|
443
|
-
attributes
|
|
444
|
-
})
|
|
445
|
-
);
|
|
446
|
-
continue;
|
|
447
|
-
}
|
|
448
|
-
if (!script.src) {
|
|
449
|
-
continue;
|
|
450
|
-
}
|
|
451
|
-
const resolvedPath = path.resolve(componentDir, script.src);
|
|
452
|
-
const key = `file:${resolvedPath}:${JSON.stringify(attributes)}`;
|
|
453
|
-
if (seenKeys.has(key)) {
|
|
454
|
-
continue;
|
|
455
|
-
}
|
|
456
|
-
seenKeys.add(key);
|
|
457
|
-
dependencies.push(
|
|
458
|
-
AssetFactory.createFileScript({
|
|
459
|
-
filepath: resolvedPath,
|
|
460
|
-
position: "head",
|
|
461
|
-
attributes
|
|
462
|
-
})
|
|
463
|
-
);
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
if (config.layout?.config) {
|
|
467
|
-
collect(config.layout.config);
|
|
468
|
-
}
|
|
469
|
-
for (const nestedComponent of config.dependencies?.components ?? []) {
|
|
470
|
-
collect(nestedComponent?.config);
|
|
471
|
-
}
|
|
472
|
-
};
|
|
473
|
-
for (const component of components) {
|
|
474
|
-
collect(component.config);
|
|
475
|
-
}
|
|
476
|
-
return dependencies;
|
|
477
|
-
}
|
|
478
448
|
async buildRouteRenderAssets(pagePath) {
|
|
479
449
|
try {
|
|
480
450
|
const pageModule = await this.importPageFile(pagePath);
|
|
481
|
-
const shouldHydrate =
|
|
451
|
+
const shouldHydrate = this.explicitGraphEnabled ? true : this.pageModuleService.shouldHydratePage(pageModule);
|
|
482
452
|
if (!shouldHydrate) {
|
|
483
453
|
return [];
|
|
484
454
|
}
|
|
@@ -490,7 +460,11 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
490
460
|
declaredModules
|
|
491
461
|
);
|
|
492
462
|
if (isMdx) {
|
|
493
|
-
const mdxConfigAssets = await this.processMdxConfigDependencies(
|
|
463
|
+
const mdxConfigAssets = await this.mdxConfigDependencyService.processMdxConfigDependencies({
|
|
464
|
+
pagePath,
|
|
465
|
+
config: pageModule.config,
|
|
466
|
+
processComponentDependencies: async (components) => await this.processComponentDependencies(components)
|
|
467
|
+
});
|
|
494
468
|
return [...processedAssets, ...mdxConfigAssets];
|
|
495
469
|
}
|
|
496
470
|
return processedAssets;
|
|
@@ -524,8 +498,11 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
524
498
|
pageProps
|
|
525
499
|
}) {
|
|
526
500
|
try {
|
|
527
|
-
const safeLocals = this.getSerializableLocals(
|
|
528
|
-
|
|
501
|
+
const safeLocals = this.pagePayloadService.getSerializableLocals(
|
|
502
|
+
locals,
|
|
503
|
+
Page.requires
|
|
504
|
+
);
|
|
505
|
+
const allPageProps = this.pagePayloadService.buildSerializedPageProps({
|
|
529
506
|
pageProps,
|
|
530
507
|
params,
|
|
531
508
|
query,
|
|
@@ -543,7 +520,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
543
520
|
htmlTemplate: HtmlTemplate,
|
|
544
521
|
metadata,
|
|
545
522
|
pageProps: allPageProps,
|
|
546
|
-
documentProps:
|
|
523
|
+
documentProps: this.buildNonReactDocumentProps(HtmlTemplate, allPageProps)
|
|
547
524
|
});
|
|
548
525
|
} catch (error) {
|
|
549
526
|
throw this.createRenderError("Failed to render component", error);
|
|
@@ -552,45 +529,6 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
552
529
|
getDocumentAttributes() {
|
|
553
530
|
return this.getRouterDocumentAttributes();
|
|
554
531
|
}
|
|
555
|
-
/**
|
|
556
|
-
* Safely extracts the declared subset of locals for client-side hydration.
|
|
557
|
-
*
|
|
558
|
-
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
559
|
-
* request-scoped data (e.g., session). Only keys explicitly declared via
|
|
560
|
-
* `Page.requires` are serialized to the client so sensitive request-only data
|
|
561
|
-
* is not leaked into hydration payloads by default.
|
|
562
|
-
*
|
|
563
|
-
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
564
|
-
* to prevent accidental use. This method safely detects that case and returns
|
|
565
|
-
* `undefined` instead of throwing.
|
|
566
|
-
*
|
|
567
|
-
* @param locals - The locals object from the render context
|
|
568
|
-
* @param requiredLocals - Keys explicitly requested for client hydration
|
|
569
|
-
* @returns The filtered locals object if serializable, undefined otherwise
|
|
570
|
-
*/
|
|
571
|
-
getSerializableLocals(locals, requiredLocals) {
|
|
572
|
-
try {
|
|
573
|
-
if (!locals) {
|
|
574
|
-
return void 0;
|
|
575
|
-
}
|
|
576
|
-
const requiredKeys = requiredLocals ? Array.isArray(requiredLocals) ? requiredLocals : [requiredLocals] : [];
|
|
577
|
-
if (requiredKeys.length === 0) {
|
|
578
|
-
return void 0;
|
|
579
|
-
}
|
|
580
|
-
const serializedLocals = Object.fromEntries(
|
|
581
|
-
requiredKeys.filter((key) => Object.prototype.hasOwnProperty.call(locals, key)).map((key) => [key, locals[key]])
|
|
582
|
-
);
|
|
583
|
-
if (Object.keys(serializedLocals).length > 0) {
|
|
584
|
-
return serializedLocals;
|
|
585
|
-
}
|
|
586
|
-
return void 0;
|
|
587
|
-
} catch (e) {
|
|
588
|
-
if (e instanceof LocalsAccessError) {
|
|
589
|
-
return void 0;
|
|
590
|
-
}
|
|
591
|
-
throw e;
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
532
|
/**
|
|
595
533
|
* Renders an arbitrary React view through the application's HTML shell.
|
|
596
534
|
*
|
|
@@ -601,6 +539,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
601
539
|
*/
|
|
602
540
|
async renderToResponse(view, props, ctx) {
|
|
603
541
|
try {
|
|
542
|
+
const { react, reactDomServer } = this.getReactRuntimeModules();
|
|
604
543
|
const viewConfig = view.config;
|
|
605
544
|
const Layout = viewConfig?.layout;
|
|
606
545
|
const ViewComponent = this.asReactComponent(view);
|
|
@@ -610,7 +549,9 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
610
549
|
view,
|
|
611
550
|
props,
|
|
612
551
|
ctx,
|
|
613
|
-
renderInline: async () => await renderToReadableStream(
|
|
552
|
+
renderInline: async () => await reactDomServer.renderToReadableStream(
|
|
553
|
+
react.createElement(ViewComponent, normalizedProps)
|
|
554
|
+
)
|
|
614
555
|
});
|
|
615
556
|
}
|
|
616
557
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
@@ -631,7 +572,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
631
572
|
props: {
|
|
632
573
|
metadata,
|
|
633
574
|
pageProps: normalizedProps,
|
|
634
|
-
|
|
575
|
+
...this.buildNonReactDocumentProps(HtmlTemplate, normalizedProps) ?? {}
|
|
635
576
|
},
|
|
636
577
|
children: layoutRender?.html ?? viewRender.html
|
|
637
578
|
});
|