@ecopages/react 0.2.0-alpha.9 → 0.2.1
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 +13 -11
- package/README.md +10 -0
- package/package.json +6 -6
- package/src/react-hmr-strategy.d.ts +4 -2
- package/src/react-hmr-strategy.js +36 -3
- package/src/react-renderer.d.ts +25 -37
- package/src/react-renderer.js +190 -142
- package/src/react.plugin.d.ts +0 -12
- package/src/react.plugin.js +2 -13
- package/src/services/react-bundle.service.d.ts +3 -1
- package/src/services/react-bundle.service.js +20 -2
- 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 +7 -6
- package/src/services/react-hydration-asset.service.js +26 -14
- package/src/services/react-page-module.service.js +5 -2
- package/src/services/react-runtime-bundle.service.d.ts +2 -0
- package/src/services/react-runtime-bundle.service.js +5 -0
- package/src/utils/client-graph-boundary-plugin.js +2 -2
- package/src/utils/declared-modules.js +4 -1
- package/src/utils/hydration-scripts.d.ts +1 -3
- package/src/utils/hydration-scripts.js +31 -19
- 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.ts +0 -45
- package/src/utils/use-sync-external-store-shim-plugin.ts +0 -45
package/src/react-renderer.js
CHANGED
|
@@ -6,14 +6,14 @@ import { rapidhash } from "@ecopages/core/hash";
|
|
|
6
6
|
import { AssetFactory } from "@ecopages/core/services/asset-processing-service";
|
|
7
7
|
import { ECO_DOCUMENT_OWNER_ATTRIBUTE } from "@ecopages/core/router/navigation-coordinator";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { createElement } from "react";
|
|
9
|
+
import { createElement, Fragment } from "react";
|
|
10
10
|
import { renderToReadableStream, renderToString } from "react-dom/server";
|
|
11
11
|
import { PLUGIN_NAME } from "./react.plugin.js";
|
|
12
12
|
import { hasSingleRootElement } from "./utils/html-boundary.js";
|
|
13
13
|
import { ReactBundleService } from "./services/react-bundle.service.js";
|
|
14
14
|
import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
|
|
15
15
|
import { ReactPageModuleService } from "./services/react-page-module.service.js";
|
|
16
|
-
import { ReactHydrationAssetService } from "./services/react-hydration-asset.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);
|
|
@@ -53,7 +53,9 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
53
53
|
this.bundleService = new ReactBundleService({
|
|
54
54
|
rootDir: this.appConfig.rootDir,
|
|
55
55
|
routerAdapter: ReactRenderer.routerAdapter,
|
|
56
|
-
mdxCompilerOptions: ReactRenderer.mdxCompilerOptions
|
|
56
|
+
mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
|
|
57
|
+
jsxImportSource: (this.appConfig.integrations ?? []).find((integration) => integration.name === this.name)?.jsxImportSource,
|
|
58
|
+
nonReactExtensions: (this.appConfig.integrations ?? []).filter((integration) => integration.name !== this.name).flatMap((integration) => integration.extensions)
|
|
57
59
|
});
|
|
58
60
|
this.pageModuleService = new ReactPageModuleService({
|
|
59
61
|
rootDir: this.appConfig.rootDir,
|
|
@@ -104,7 +106,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
104
106
|
*
|
|
105
107
|
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
106
108
|
* page-data contract as fully React-owned documents so navigation and hydration
|
|
107
|
-
* can read one
|
|
109
|
+
* can read one shared document payload consistently.
|
|
108
110
|
*/
|
|
109
111
|
buildRouterPageDataScript(pageProps) {
|
|
110
112
|
const safeJson = JSON.stringify(pageProps || {}).replace(/</g, "\\u003c");
|
|
@@ -163,25 +165,7 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
165
167
|
const hydrationAssets = await this.buildRouteRenderAssets(filePath);
|
|
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;
|
|
168
|
+
this.appendProcessedDependencies(hydrationAssets);
|
|
185
169
|
}
|
|
186
170
|
/**
|
|
187
171
|
* Renders a non-React layout or HTML template and enforces that mixed shells
|
|
@@ -198,64 +182,77 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
198
182
|
throw new ReactRenderError(`${label} must return a string when used as a mixed shell for React pages.`);
|
|
199
183
|
}
|
|
200
184
|
/**
|
|
201
|
-
*
|
|
185
|
+
* Renders one React component boundary while preserving already-resolved child HTML.
|
|
202
186
|
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
187
|
+
* When nested boundary resolution has already produced child HTML for this
|
|
188
|
+
* boundary, the child payload must remain raw SSR output rather than a React
|
|
189
|
+
* string child, otherwise React would escape it. This helper renders a unique
|
|
190
|
+
* token through React and swaps that token back to the resolved HTML
|
|
191
|
+
* afterward.
|
|
192
|
+
*
|
|
193
|
+
* @param input Component render input for the current boundary.
|
|
194
|
+
* @param context React-specific render context for stable token generation.
|
|
195
|
+
* @returns Serialized component HTML with resolved child markup preserved.
|
|
206
196
|
*/
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return { contentNode: pageElement, contentHtml: pageHtml };
|
|
197
|
+
renderComponentHtml(input, context, runtimeContext) {
|
|
198
|
+
if (input.children === void 0) {
|
|
199
|
+
return this.normalizeBoundaryArtifactHtml(
|
|
200
|
+
renderToString(createElement(this.asReactComponent(input.component), input.props))
|
|
201
|
+
);
|
|
213
202
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
};
|
|
203
|
+
const resolvedChildHtml = typeof input.children === "string" ? input.children : String(input.children ?? "");
|
|
204
|
+
const rawChildrenToken = `__ECO_RAW_HTML_CHILD_${context.componentInstanceId ?? "component"}__`;
|
|
205
|
+
if (runtimeContext) {
|
|
206
|
+
runtimeContext.rawChildrenToken = rawChildrenToken;
|
|
207
|
+
runtimeContext.rawChildrenHtml = resolvedChildHtml;
|
|
220
208
|
}
|
|
221
|
-
const
|
|
222
|
-
this.
|
|
223
|
-
{ ...layoutProps, children: pageHtml },
|
|
224
|
-
"Layout"
|
|
209
|
+
const html = renderToString(
|
|
210
|
+
createElement(this.asReactComponent(input.component), input.props, rawChildrenToken)
|
|
225
211
|
);
|
|
226
|
-
return
|
|
212
|
+
return this.normalizeBoundaryArtifactHtml(html.split(rawChildrenToken).join(resolvedChildHtml));
|
|
227
213
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return renderToReadableStream(
|
|
238
|
-
createElement(
|
|
239
|
-
this.asReactComponent(options.HtmlTemplate),
|
|
240
|
-
{
|
|
241
|
-
metadata: options.metadata,
|
|
242
|
-
pageProps: options.pageProps
|
|
243
|
-
},
|
|
244
|
-
options.contentNode
|
|
245
|
-
)
|
|
246
|
-
);
|
|
214
|
+
restoreRuntimeChildHtml(html, runtimeContext) {
|
|
215
|
+
if (!runtimeContext?.rawChildrenToken || runtimeContext.rawChildrenHtml === void 0) {
|
|
216
|
+
return html;
|
|
217
|
+
}
|
|
218
|
+
return html.split(runtimeContext.rawChildrenToken).join(runtimeContext.rawChildrenHtml);
|
|
219
|
+
}
|
|
220
|
+
async renderQueuedChildrenToHtml(children, runtimeContext, queuedResolutionsByToken, resolveToken) {
|
|
221
|
+
if (children === void 0) {
|
|
222
|
+
return void 0;
|
|
247
223
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
this.asNonReactShellComponent(options.HtmlTemplate),
|
|
251
|
-
{
|
|
252
|
-
metadata: options.metadata,
|
|
253
|
-
pageProps: options.pageProps,
|
|
254
|
-
children: options.contentHtml,
|
|
255
|
-
headContent
|
|
256
|
-
},
|
|
257
|
-
"HtmlTemplate"
|
|
224
|
+
let html = this.normalizeBoundaryArtifactHtml(
|
|
225
|
+
renderToString(createElement(Fragment, null, children))
|
|
258
226
|
);
|
|
227
|
+
html = this.restoreRuntimeChildHtml(html, runtimeContext);
|
|
228
|
+
html = await this.resolveQueuedBoundaryTokens(html, queuedResolutionsByToken, resolveToken);
|
|
229
|
+
return html;
|
|
230
|
+
}
|
|
231
|
+
async resolveQueuedBoundaryHtml(html, runtimeContext) {
|
|
232
|
+
return this.resolveRendererOwnedQueuedBoundaryHtml({
|
|
233
|
+
html,
|
|
234
|
+
runtimeContext,
|
|
235
|
+
queueLabel: "React",
|
|
236
|
+
renderQueuedChildren: async (children, currentRuntimeContext, queuedResolutionsByToken, resolveToken) => {
|
|
237
|
+
const renderedHtml = await this.renderQueuedChildrenToHtml(
|
|
238
|
+
children,
|
|
239
|
+
currentRuntimeContext,
|
|
240
|
+
queuedResolutionsByToken,
|
|
241
|
+
resolveToken
|
|
242
|
+
);
|
|
243
|
+
return {
|
|
244
|
+
assets: [],
|
|
245
|
+
html: renderedHtml
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
buildHydrationProps(props) {
|
|
251
|
+
if (!props || !Object.prototype.hasOwnProperty.call(props, "locals")) {
|
|
252
|
+
return props ?? {};
|
|
253
|
+
}
|
|
254
|
+
const { locals: _locals, ...hydrationProps } = props;
|
|
255
|
+
return hydrationProps;
|
|
259
256
|
}
|
|
260
257
|
/**
|
|
261
258
|
* Renders a React component for component-level orchestration.
|
|
@@ -265,43 +262,90 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
265
262
|
* - When an explicit component instance id is provided, a stable
|
|
266
263
|
* `data-eco-component-id` attribute is attached so island hydration can target it.
|
|
267
264
|
* - Without an explicit instance id, component renders remain plain SSR output.
|
|
265
|
+
* - When resolved child HTML is provided, that boundary is treated as a pure SSR
|
|
266
|
+
* composition step and does not emit hydration assets for the parent wrapper.
|
|
268
267
|
*
|
|
269
268
|
* This preserves DOM shape for global CSS/layout selectors while keeping a
|
|
270
269
|
* deterministic mount target per component instance.
|
|
271
270
|
*/
|
|
272
271
|
async renderComponent(input) {
|
|
273
|
-
const
|
|
272
|
+
const runtimeContext = this.getQueuedBoundaryRuntime(input);
|
|
273
|
+
if (!this.isReactManagedComponent(input.component)) {
|
|
274
|
+
let props = input.props;
|
|
275
|
+
if (input.children !== void 0) {
|
|
276
|
+
props = {
|
|
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
|
|
300
|
+
};
|
|
301
|
+
}
|
|
274
302
|
const componentConfig = input.component.config;
|
|
275
|
-
const
|
|
276
|
-
|
|
303
|
+
const context = input.integrationContext ?? {};
|
|
304
|
+
const hasResolvedChildHtml = input.children !== void 0;
|
|
305
|
+
let html = this.renderComponentHtml(input, context, runtimeContext);
|
|
306
|
+
const queuedBoundaryResolution = await this.resolveQueuedBoundaryHtml(html, runtimeContext);
|
|
307
|
+
html = queuedBoundaryResolution.html;
|
|
277
308
|
let canAttachAttributes = hasSingleRootElement(html);
|
|
278
309
|
let rootTag = this.getRootTagName(html);
|
|
279
310
|
const componentFile = componentConfig?.__eco?.file;
|
|
280
|
-
const context = input.integrationContext ?? {};
|
|
281
311
|
let rootAttributes;
|
|
282
312
|
let assets;
|
|
283
|
-
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService) {
|
|
313
|
+
if (canAttachAttributes && componentFile && context.componentInstanceId && this.assetProcessingService && !hasResolvedChildHtml) {
|
|
284
314
|
const componentInstanceId = context.componentInstanceId;
|
|
285
|
-
assets = await this.hydrationAssetService.buildComponentRenderAssets(
|
|
286
|
-
componentFile,
|
|
287
|
-
componentInstanceId,
|
|
288
|
-
input.props,
|
|
289
|
-
componentConfig
|
|
290
|
-
);
|
|
315
|
+
assets = await this.hydrationAssetService.buildComponentRenderAssets(componentFile, componentConfig);
|
|
291
316
|
rootAttributes = {
|
|
292
317
|
"data-eco-component-id": componentInstanceId,
|
|
293
|
-
"data-eco-
|
|
318
|
+
"data-eco-component-key": getReactIslandComponentKey(componentFile, componentConfig),
|
|
319
|
+
"data-eco-props": btoa(JSON.stringify(this.buildHydrationProps(input.props)))
|
|
294
320
|
};
|
|
295
321
|
}
|
|
322
|
+
const mergedAssets = this.htmlTransformer.dedupeProcessedAssets([
|
|
323
|
+
...assets ?? [],
|
|
324
|
+
...queuedBoundaryResolution.assets
|
|
325
|
+
]);
|
|
296
326
|
return {
|
|
297
327
|
html,
|
|
298
328
|
canAttachAttributes,
|
|
299
329
|
rootTag,
|
|
300
330
|
integrationName: this.name,
|
|
301
331
|
rootAttributes,
|
|
302
|
-
assets
|
|
332
|
+
assets: mergedAssets.length > 0 ? mergedAssets : void 0
|
|
303
333
|
};
|
|
304
334
|
}
|
|
335
|
+
createComponentBoundaryRuntime(options) {
|
|
336
|
+
return this.createQueuedBoundaryRuntime({
|
|
337
|
+
boundaryInput: options.boundaryInput,
|
|
338
|
+
rendererCache: options.rendererCache,
|
|
339
|
+
createRuntimeContext: (integrationContext, rendererCache) => ({
|
|
340
|
+
rendererCache,
|
|
341
|
+
componentInstanceScope: integrationContext.componentInstanceId,
|
|
342
|
+
nextBoundaryId: 0,
|
|
343
|
+
queuedResolutions: [],
|
|
344
|
+
rawChildrenToken: void 0,
|
|
345
|
+
rawChildrenHtml: void 0
|
|
346
|
+
})
|
|
347
|
+
});
|
|
348
|
+
}
|
|
305
349
|
/**
|
|
306
350
|
* Checks if the given file path corresponds to an MDX file based on configured extensions.
|
|
307
351
|
* @param filePath - The file path to check
|
|
@@ -310,13 +354,33 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
310
354
|
isMdxFile(filePath) {
|
|
311
355
|
return this.pageModuleService.isMdxFile(filePath);
|
|
312
356
|
}
|
|
357
|
+
usesIntegrationPageImporter(file) {
|
|
358
|
+
return this.pageModuleService.isMdxFile(file);
|
|
359
|
+
}
|
|
360
|
+
async importIntegrationPageFile(file) {
|
|
361
|
+
return await this.pageModuleService.importMdxPageFile(file);
|
|
362
|
+
}
|
|
363
|
+
normalizeImportedPageFile(file, pageModule) {
|
|
364
|
+
const reactModule = pageModule;
|
|
365
|
+
const { default: Page, getMetadata, config } = reactModule;
|
|
366
|
+
if (this.pageModuleService.isMdxFile(file) && config) {
|
|
367
|
+
Page.config = config;
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
...pageModule,
|
|
371
|
+
default: Page,
|
|
372
|
+
getMetadata,
|
|
373
|
+
config
|
|
374
|
+
};
|
|
375
|
+
}
|
|
313
376
|
/**
|
|
314
377
|
* Processes MDX-specific configuration dependencies including layout dependencies.
|
|
315
378
|
* @param pagePath - Absolute path to the MDX page file
|
|
316
379
|
* @returns Processed assets for MDX configuration dependencies
|
|
317
380
|
*/
|
|
318
381
|
async processMdxConfigDependencies(pagePath) {
|
|
319
|
-
const
|
|
382
|
+
const pageModule = await this.importPageFile(pagePath);
|
|
383
|
+
const config = pageModule.config;
|
|
320
384
|
const resolvedLayout = config?.layout;
|
|
321
385
|
const components = [];
|
|
322
386
|
if (resolvedLayout?.config?.dependencies) {
|
|
@@ -439,26 +503,6 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
439
503
|
);
|
|
440
504
|
}
|
|
441
505
|
}
|
|
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
506
|
/**
|
|
463
507
|
* Renders a full route response for the filesystem page pipeline.
|
|
464
508
|
*
|
|
@@ -487,18 +531,19 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
487
531
|
query,
|
|
488
532
|
safeLocals
|
|
489
533
|
});
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
534
|
+
return await this.renderPageWithDocumentShell({
|
|
535
|
+
page: {
|
|
536
|
+
component: Page,
|
|
537
|
+
props: { params, query, ...props, locals: pageLocals }
|
|
538
|
+
},
|
|
539
|
+
layout: Layout ? {
|
|
540
|
+
component: Layout,
|
|
541
|
+
props: locals ? { locals } : {}
|
|
542
|
+
} : void 0,
|
|
543
|
+
htmlTemplate: HtmlTemplate,
|
|
498
544
|
metadata,
|
|
499
545
|
pageProps: allPageProps,
|
|
500
|
-
|
|
501
|
-
contentHtml
|
|
546
|
+
documentProps: !this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(allPageProps) } : void 0
|
|
502
547
|
});
|
|
503
548
|
} catch (error) {
|
|
504
549
|
throw this.createRenderError("Failed to render component", error);
|
|
@@ -561,38 +606,41 @@ class ReactRenderer extends IntegrationRenderer {
|
|
|
561
606
|
const ViewComponent = this.asReactComponent(view);
|
|
562
607
|
const normalizedProps = props ?? {};
|
|
563
608
|
if (ctx.partial) {
|
|
564
|
-
|
|
565
|
-
|
|
609
|
+
return this.renderPartialViewResponse({
|
|
610
|
+
view,
|
|
611
|
+
props,
|
|
612
|
+
ctx,
|
|
613
|
+
renderInline: async () => await renderToReadableStream(createElement(ViewComponent, normalizedProps))
|
|
614
|
+
});
|
|
566
615
|
}
|
|
567
616
|
const HtmlTemplate = await this.getHtmlTemplate();
|
|
568
617
|
const metadata = await this.resolveViewMetadata(view, props);
|
|
569
618
|
await this.prepareViewDependencies(view, Layout);
|
|
570
619
|
await this.appendHydrationAssetsForFile(viewConfig?.__eco?.file);
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
pageProps: normalizedProps
|
|
620
|
+
const viewRender = await this.renderComponentBoundary({
|
|
621
|
+
component: view,
|
|
622
|
+
props: normalizedProps
|
|
575
623
|
});
|
|
576
|
-
const
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
624
|
+
const layoutRender = Layout ? await this.renderComponentBoundary({
|
|
625
|
+
component: Layout,
|
|
626
|
+
props: {},
|
|
627
|
+
children: viewRender.html
|
|
628
|
+
}) : void 0;
|
|
629
|
+
const documentRender = await this.renderComponentBoundary({
|
|
630
|
+
component: HtmlTemplate,
|
|
631
|
+
props: {
|
|
632
|
+
metadata,
|
|
633
|
+
pageProps: normalizedProps,
|
|
634
|
+
...!this.isReactManagedComponent(HtmlTemplate) && ReactRenderer.routerAdapter ? { headContent: this.buildRouterPageDataScript(normalizedProps) } : {}
|
|
635
|
+
},
|
|
636
|
+
children: layoutRender?.html ?? viewRender.html
|
|
637
|
+
});
|
|
638
|
+
this.appendProcessedDependencies(viewRender.assets, layoutRender?.assets, documentRender.assets);
|
|
639
|
+
const transformedHtml = await this.finalizeResolvedHtml({
|
|
640
|
+
html: `${this.DOC_TYPE}${documentRender.html}`,
|
|
641
|
+
partial: false,
|
|
642
|
+
documentAttributes: this.getRouterDocumentAttributes()
|
|
582
643
|
});
|
|
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
644
|
return this.createHtmlResponse(transformedHtml, ctx);
|
|
597
645
|
} catch (error) {
|
|
598
646
|
throw this.createRenderError("Failed to render view", error);
|
package/src/react.plugin.d.ts
CHANGED
|
@@ -10,7 +10,6 @@ import type { CompileOptions } from '@mdx-js/mdx';
|
|
|
10
10
|
import type React from 'react';
|
|
11
11
|
import { ReactRenderer } from './react-renderer.js';
|
|
12
12
|
import type { ReactRouterAdapter } from './router-adapter.js';
|
|
13
|
-
import type { ComponentBoundaryPolicyInput } from '@ecopages/core/plugins/integration-plugin';
|
|
14
13
|
/**
|
|
15
14
|
* MDX configuration options for the React plugin
|
|
16
15
|
*/
|
|
@@ -139,17 +138,6 @@ export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
|
|
|
139
138
|
*/
|
|
140
139
|
getHmrStrategy(): HmrStrategy | undefined;
|
|
141
140
|
getRuntimeSpecifierMap(): Record<string, string>;
|
|
142
|
-
/**
|
|
143
|
-
* Declares React's boundary deferral rule for cross-integration rendering.
|
|
144
|
-
*
|
|
145
|
-
* React defers when a render pass owned by another integration enters a React
|
|
146
|
-
* component boundary. That boundary is then resolved later through the marker
|
|
147
|
-
* graph stage using the React renderer.
|
|
148
|
-
*
|
|
149
|
-
* @param input Boundary metadata for the active render pass.
|
|
150
|
-
* @returns `true` when the boundary should be deferred into the marker pass.
|
|
151
|
-
*/
|
|
152
|
-
shouldDeferComponentBoundary(input: ComponentBoundaryPolicyInput): boolean;
|
|
153
141
|
}
|
|
154
142
|
/**
|
|
155
143
|
* Factory function to create a React plugin instance
|
package/src/react.plugin.js
CHANGED
|
@@ -38,6 +38,7 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
38
38
|
super({
|
|
39
39
|
name: PLUGIN_NAME,
|
|
40
40
|
extensions,
|
|
41
|
+
jsxImportSource: "react",
|
|
41
42
|
...restOptions
|
|
42
43
|
});
|
|
43
44
|
this.mdxEnabled = options?.mdx?.enabled ?? false;
|
|
@@ -70,6 +71,7 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
70
71
|
if (this.runtimeDependenciesInitialized) {
|
|
71
72
|
return;
|
|
72
73
|
}
|
|
74
|
+
this.runtimeBundleService.setRootDir(this.appConfig?.rootDir);
|
|
73
75
|
this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
|
|
74
76
|
this.runtimeDependenciesInitialized = true;
|
|
75
77
|
}
|
|
@@ -133,19 +135,6 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
133
135
|
getRuntimeSpecifierMap() {
|
|
134
136
|
return this.runtimeBundleService.getSpecifierMap();
|
|
135
137
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Declares React's boundary deferral rule for cross-integration rendering.
|
|
138
|
-
*
|
|
139
|
-
* React defers when a render pass owned by another integration enters a React
|
|
140
|
-
* component boundary. That boundary is then resolved later through the marker
|
|
141
|
-
* graph stage using the React renderer.
|
|
142
|
-
*
|
|
143
|
-
* @param input Boundary metadata for the active render pass.
|
|
144
|
-
* @returns `true` when the boundary should be deferred into the marker pass.
|
|
145
|
-
*/
|
|
146
|
-
shouldDeferComponentBoundary(input) {
|
|
147
|
-
return input.targetIntegration === this.name && input.currentIntegration !== this.name;
|
|
148
|
-
}
|
|
149
138
|
}
|
|
150
139
|
function reactPlugin(options) {
|
|
151
140
|
return new ReactPlugin(options);
|
|
@@ -16,6 +16,8 @@ export interface ReactBundleServiceConfig {
|
|
|
16
16
|
rootDir: string;
|
|
17
17
|
routerAdapter?: ReactRouterAdapter;
|
|
18
18
|
mdxCompilerOptions?: CompileOptions;
|
|
19
|
+
nonReactExtensions?: string[];
|
|
20
|
+
jsxImportSource?: string;
|
|
19
21
|
}
|
|
20
22
|
/**
|
|
21
23
|
* Manages esbuild bundle configuration and plugin creation for React page/component builds.
|
|
@@ -41,5 +43,5 @@ export declare class ReactBundleService {
|
|
|
41
43
|
* Creates the esbuild plugin that rewrites bare React specifiers
|
|
42
44
|
* to their runtime asset URLs.
|
|
43
45
|
*/
|
|
44
|
-
createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("
|
|
46
|
+
createRuntimeAliasPlugin(runtimeSpecifierMap: Record<string, string>): import("@ecopages/core/build/build-types").EcoBuildPlugin | null;
|
|
45
47
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "../utils/react-runtime-specifier-map.js";
|
|
7
7
|
import { createUseSyncExternalStoreShimPlugin } from "../utils/use-sync-external-store-shim-plugin.js";
|
|
8
8
|
import { createRuntimeSpecifierAliasPlugin } from "@ecopages/core/build/runtime-specifier-alias-plugin";
|
|
9
|
+
import { createForeignJsxOverridePlugin } from "@ecopages/core/plugins/foreign-jsx-override-plugin";
|
|
9
10
|
import { ReactRuntimeBundleService } from "./react-runtime-bundle.service.js";
|
|
10
11
|
class ReactBundleService {
|
|
11
12
|
runtimeBundleService;
|
|
@@ -13,6 +14,7 @@ class ReactBundleService {
|
|
|
13
14
|
constructor(config) {
|
|
14
15
|
this.config = config;
|
|
15
16
|
this.runtimeBundleService = new ReactRuntimeBundleService({
|
|
17
|
+
rootDir: config.rootDir,
|
|
16
18
|
routerAdapter: config.routerAdapter
|
|
17
19
|
});
|
|
18
20
|
}
|
|
@@ -48,6 +50,11 @@ class ReactBundleService {
|
|
|
48
50
|
declaredModules,
|
|
49
51
|
alwaysAllowSpecifiers: getReactClientGraphAllowSpecifiers([], this.config.routerAdapter)
|
|
50
52
|
});
|
|
53
|
+
const foreignJsxOverridePlugin = createForeignJsxOverridePlugin({
|
|
54
|
+
name: "react-renderer-foreign-jsx-override",
|
|
55
|
+
hostJsxImportSource: this.config.jsxImportSource ?? "react",
|
|
56
|
+
foreignExtensions: this.config.nonReactExtensions ?? []
|
|
57
|
+
});
|
|
51
58
|
const runtimeAliasPlugin = this.createRuntimeAliasPlugin(runtimeSpecifierMap);
|
|
52
59
|
const useSyncExternalStoreShimPlugin = createUseSyncExternalStoreShimPlugin({
|
|
53
60
|
name: "react-renderer-use-sync-external-store-shim",
|
|
@@ -56,9 +63,20 @@ class ReactBundleService {
|
|
|
56
63
|
if (isMdx && this.config.mdxCompilerOptions) {
|
|
57
64
|
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
58
65
|
const mdxPlugin = createReactMdxLoaderPlugin(this.config.mdxCompilerOptions);
|
|
59
|
-
options.plugins = [
|
|
66
|
+
options.plugins = [
|
|
67
|
+
foreignJsxOverridePlugin,
|
|
68
|
+
graphBoundaryPlugin,
|
|
69
|
+
runtimeAliasPlugin,
|
|
70
|
+
mdxPlugin,
|
|
71
|
+
useSyncExternalStoreShimPlugin
|
|
72
|
+
];
|
|
60
73
|
} else {
|
|
61
|
-
options.plugins = [
|
|
74
|
+
options.plugins = [
|
|
75
|
+
foreignJsxOverridePlugin,
|
|
76
|
+
graphBoundaryPlugin,
|
|
77
|
+
runtimeAliasPlugin,
|
|
78
|
+
useSyncExternalStoreShimPlugin
|
|
79
|
+
];
|
|
62
80
|
}
|
|
63
81
|
return options;
|
|
64
82
|
}
|
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
export declare class ReactHmrPageMetadataCache {
|
|
8
8
|
private readonly declaredModulesByEntrypoint;
|
|
9
|
+
private readonly ownedEntrypoints;
|
|
10
|
+
/**
|
|
11
|
+
* Marks an HMR entrypoint as React-owned.
|
|
12
|
+
*/
|
|
13
|
+
markOwnedEntrypoint(entrypointPath: string): void;
|
|
9
14
|
/**
|
|
10
15
|
* Stores the declared browser modules for a page entrypoint.
|
|
11
16
|
*/
|
|
@@ -14,4 +19,8 @@ export declare class ReactHmrPageMetadataCache {
|
|
|
14
19
|
* Returns the last known declared browser modules for a page entrypoint.
|
|
15
20
|
*/
|
|
16
21
|
getDeclaredModules(entrypointPath: string): string[] | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Returns true when the watched entrypoint is owned by the React integration.
|
|
24
|
+
*/
|
|
25
|
+
ownsEntrypoint(entrypointPath: string): boolean;
|
|
17
26
|
}
|
|
@@ -1,18 +1,34 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
class ReactHmrPageMetadataCache {
|
|
2
3
|
declaredModulesByEntrypoint = /* @__PURE__ */ new Map();
|
|
4
|
+
ownedEntrypoints = /* @__PURE__ */ new Set();
|
|
5
|
+
/**
|
|
6
|
+
* Marks an HMR entrypoint as React-owned.
|
|
7
|
+
*/
|
|
8
|
+
markOwnedEntrypoint(entrypointPath) {
|
|
9
|
+
this.ownedEntrypoints.add(path.resolve(entrypointPath));
|
|
10
|
+
}
|
|
3
11
|
/**
|
|
4
12
|
* Stores the declared browser modules for a page entrypoint.
|
|
5
13
|
*/
|
|
6
14
|
setDeclaredModules(entrypointPath, declaredModules) {
|
|
7
|
-
|
|
15
|
+
const resolvedEntrypointPath = path.resolve(entrypointPath);
|
|
16
|
+
this.markOwnedEntrypoint(resolvedEntrypointPath);
|
|
17
|
+
this.declaredModulesByEntrypoint.set(resolvedEntrypointPath, [...declaredModules]);
|
|
8
18
|
}
|
|
9
19
|
/**
|
|
10
20
|
* Returns the last known declared browser modules for a page entrypoint.
|
|
11
21
|
*/
|
|
12
22
|
getDeclaredModules(entrypointPath) {
|
|
13
|
-
const declaredModules = this.declaredModulesByEntrypoint.get(entrypointPath);
|
|
23
|
+
const declaredModules = this.declaredModulesByEntrypoint.get(path.resolve(entrypointPath));
|
|
14
24
|
return declaredModules ? [...declaredModules] : void 0;
|
|
15
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Returns true when the watched entrypoint is owned by the React integration.
|
|
28
|
+
*/
|
|
29
|
+
ownsEntrypoint(entrypointPath) {
|
|
30
|
+
return this.ownedEntrypoints.has(path.resolve(entrypointPath));
|
|
31
|
+
}
|
|
16
32
|
}
|
|
17
33
|
export {
|
|
18
34
|
ReactHmrPageMetadataCache
|