@ecopages/react 0.2.0-alpha.4 → 0.2.0-alpha.41

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.
Files changed (70) hide show
  1. package/README.md +161 -18
  2. package/package.json +16 -12
  3. package/src/eco-embed.d.ts +11 -0
  4. package/src/eco-embed.js +11 -0
  5. package/src/react-hmr-strategy.d.ts +42 -32
  6. package/src/react-hmr-strategy.js +103 -124
  7. package/src/react-renderer.d.ts +169 -42
  8. package/src/react-renderer.js +484 -164
  9. package/src/react.constants.d.ts +1 -0
  10. package/src/react.constants.js +4 -0
  11. package/src/react.plugin.d.ts +38 -111
  12. package/src/react.plugin.js +132 -61
  13. package/src/react.types.d.ts +88 -0
  14. package/src/react.types.js +0 -0
  15. package/src/router-adapter.d.ts +7 -14
  16. package/src/services/react-bundle.service.d.ts +15 -26
  17. package/src/services/react-bundle.service.js +45 -93
  18. package/src/services/react-hmr-page-metadata-cache.d.ts +9 -0
  19. package/src/services/react-hmr-page-metadata-cache.js +18 -2
  20. package/src/services/react-hydration-asset.service.d.ts +26 -19
  21. package/src/services/react-hydration-asset.service.js +72 -66
  22. package/src/services/react-mdx-config-dependency.service.d.ts +36 -0
  23. package/src/services/react-mdx-config-dependency.service.js +122 -0
  24. package/src/services/react-page-module.service.d.ts +10 -2
  25. package/src/services/react-page-module.service.js +47 -39
  26. package/src/services/react-page-payload.service.d.ts +46 -0
  27. package/src/services/react-page-payload.service.js +67 -0
  28. package/src/services/react-runtime-bundle.service.d.ts +15 -13
  29. package/src/services/react-runtime-bundle.service.js +103 -180
  30. package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
  31. package/src/utils/client-graph-boundary-plugin.js +149 -11
  32. package/src/utils/component-config-traversal.d.ts +36 -0
  33. package/src/utils/component-config-traversal.js +54 -0
  34. package/src/utils/declared-modules.d.ts +1 -1
  35. package/src/utils/declared-modules.js +7 -16
  36. package/src/utils/dynamic.test.browser.d.ts +1 -0
  37. package/src/utils/dynamic.test.browser.js +33 -0
  38. package/src/utils/hydration-scripts.d.ts +25 -6
  39. package/src/utils/hydration-scripts.js +150 -44
  40. package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
  41. package/src/utils/hydration-scripts.test.browser.js +198 -0
  42. package/src/utils/reachability-analyzer.d.ts +12 -1
  43. package/src/utils/reachability-analyzer.js +101 -5
  44. package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
  45. package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
  46. package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
  47. package/src/utils/react-mdx-loader-plugin.js +13 -5
  48. package/src/utils/react-runtime-alias-map.d.ts +6 -0
  49. package/src/utils/react-runtime-alias-map.js +33 -0
  50. package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
  51. package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
  52. package/CHANGELOG.md +0 -62
  53. package/src/react-hmr-strategy.ts +0 -444
  54. package/src/react-renderer.ts +0 -403
  55. package/src/react.plugin.ts +0 -241
  56. package/src/router-adapter.ts +0 -95
  57. package/src/services/react-bundle.service.ts +0 -212
  58. package/src/services/react-hmr-page-metadata-cache.ts +0 -24
  59. package/src/services/react-hydration-asset.service.ts +0 -260
  60. package/src/services/react-page-module.service.ts +0 -214
  61. package/src/services/react-runtime-bundle.service.ts +0 -271
  62. package/src/utils/client-graph-boundary-plugin.ts +0 -590
  63. package/src/utils/client-only.ts +0 -27
  64. package/src/utils/declared-modules.ts +0 -99
  65. package/src/utils/dynamic.ts +0 -27
  66. package/src/utils/hmr-scripts.ts +0 -47
  67. package/src/utils/html-boundary.ts +0 -66
  68. package/src/utils/hydration-scripts.ts +0 -338
  69. package/src/utils/reachability-analyzer.ts +0 -440
  70. package/src/utils/react-mdx-loader-plugin.ts +0 -40
@@ -1,403 +0,0 @@
1
- /**
2
- * This module contains the React renderer
3
- * @module
4
- */
5
-
6
- import type {
7
- ComponentRenderInput,
8
- ComponentRenderResult,
9
- EcoComponent,
10
- EcoComponentConfig,
11
- EcoPageFile,
12
- HtmlTemplateProps,
13
- IntegrationRendererRenderOptions,
14
- PageMetadataProps,
15
- RequestLocals,
16
- RouteRendererBody,
17
- } from '@ecopages/core';
18
- import { IntegrationRenderer, type RenderToResponseContext } from '@ecopages/core/route-renderer/integration-renderer';
19
- import { LocalsAccessError } from '@ecopages/core/errors/locals-access-error';
20
- import { RESOLVED_ASSETS_DIR } from '@ecopages/core/constants';
21
- import { rapidhash } from '@ecopages/core/hash';
22
- import type { ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
23
- import { createElement, type ReactNode } from 'react';
24
- import { renderToReadableStream, renderToString } from 'react-dom/server';
25
- import type { CompileOptions } from '@mdx-js/mdx';
26
- import { PLUGIN_NAME } from './react.plugin.ts';
27
- import type { ReactRouterAdapter } from './router-adapter.ts';
28
- import { hasSingleRootElement } from './utils/html-boundary.ts';
29
- import { ReactBundleService } from './services/react-bundle.service.ts';
30
- import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
31
- import { ReactPageModuleService } from './services/react-page-module.service.ts';
32
- import { ReactHydrationAssetService } from './services/react-hydration-asset.service.ts';
33
-
34
- type ReactComponentRenderContext = {
35
- componentInstanceId?: string;
36
- };
37
-
38
- /**
39
- * Error thrown when an error occurs while rendering a React component.
40
- */
41
- export class ReactRenderError extends Error {
42
- constructor(message: string) {
43
- super(message);
44
- this.name = 'ReactRenderError';
45
- }
46
- }
47
-
48
- /**
49
- * Error thrown when an error occurs while bundling a React component.
50
- */
51
- export class BundleError extends Error {
52
- constructor(
53
- message: string,
54
- public readonly logs: string[],
55
- ) {
56
- super(message);
57
- this.name = 'BundleError';
58
- }
59
- }
60
-
61
- /**
62
- * Renderer for React components.
63
- * @extends IntegrationRenderer
64
- */
65
- export class ReactRenderer extends IntegrationRenderer<ReactNode> {
66
- name = PLUGIN_NAME;
67
- componentDirectory = RESOLVED_ASSETS_DIR;
68
- private componentRenderSequence = 0;
69
- static routerAdapter: ReactRouterAdapter | undefined;
70
- static mdxCompilerOptions: CompileOptions | undefined;
71
- static mdxExtensions: string[] = ['.mdx'];
72
- static hmrPageMetadataCache: ReactHmrPageMetadataCache | undefined;
73
- /**
74
- * Enables explicit graph behavior for React page-entry bundling.
75
- *
76
- * When true, page-entry bundles disable AST server-only stripping and rely
77
- * on explicit dependency declarations for browser graph composition.
78
- */
79
- static explicitGraphEnabled = false;
80
-
81
- /** @internal */
82
- readonly bundleService: ReactBundleService;
83
- /** @internal */
84
- readonly pageModuleService: ReactPageModuleService;
85
- /** @internal */
86
- readonly hydrationAssetService: ReactHydrationAssetService;
87
-
88
- constructor(options: {
89
- appConfig: ConstructorParameters<typeof IntegrationRenderer>[0]['appConfig'];
90
- assetProcessingService: ConstructorParameters<typeof IntegrationRenderer>[0]['assetProcessingService'];
91
- resolvedIntegrationDependencies?: ProcessedAsset[];
92
- runtimeOrigin: string;
93
- }) {
94
- super(options);
95
-
96
- this.bundleService = new ReactBundleService({
97
- rootDir: this.appConfig.rootDir,
98
- routerAdapter: ReactRenderer.routerAdapter,
99
- mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
100
- });
101
-
102
- this.pageModuleService = new ReactPageModuleService({
103
- rootDir: this.appConfig.rootDir,
104
- distDir: this.appConfig.absolutePaths.distDir,
105
- layoutsDir: this.appConfig.absolutePaths.layoutsDir,
106
- componentsDir: this.appConfig.absolutePaths.componentsDir,
107
- mdxCompilerOptions: ReactRenderer.mdxCompilerOptions,
108
- mdxExtensions: ReactRenderer.mdxExtensions,
109
- integrationName: this.name,
110
- hasRouterAdapter: Boolean(ReactRenderer.routerAdapter),
111
- });
112
-
113
- this.hydrationAssetService = new ReactHydrationAssetService({
114
- srcDir: this.appConfig.srcDir,
115
- routerAdapter: ReactRenderer.routerAdapter,
116
- assetProcessingService: this.assetProcessingService,
117
- bundleService: this.bundleService,
118
- hmrPageMetadataCache: ReactRenderer.hmrPageMetadataCache,
119
- });
120
- }
121
-
122
- protected override shouldRenderPageComponent(): boolean {
123
- return false;
124
- }
125
-
126
- /**
127
- * Renders a React component for component-level orchestration.
128
- *
129
- * Behavior:
130
- * - SSR always returns the component's own root HTML (no synthetic wrapper).
131
- * - For single-root output, a stable `data-eco-component-id` attribute is attached
132
- * to the root element so the client island runtime can target it directly.
133
- * - Island client scripts are emitted through `assets` and mounted independently.
134
- *
135
- * This preserves DOM shape for global CSS/layout selectors while keeping a
136
- * deterministic mount target per component instance.
137
- */
138
- override async renderComponent(input: ComponentRenderInput): Promise<ComponentRenderResult> {
139
- const Component = input.component as unknown as React.FunctionComponent;
140
- const componentConfig = input.component.config;
141
- const element =
142
- input.children === undefined
143
- ? createElement(Component, input.props)
144
- : createElement(Component, input.props, input.children);
145
- let html = renderToString(element);
146
- let canAttachAttributes = hasSingleRootElement(html);
147
- let rootTag = this.getRootTagName(html);
148
- const componentFile = componentConfig?.__eco?.file;
149
- const context = (input.integrationContext as ReactComponentRenderContext | undefined) ?? {};
150
-
151
- let rootAttributes: Record<string, string> | undefined;
152
- let assets: ProcessedAsset[] | undefined;
153
-
154
- if (canAttachAttributes && componentFile && this.assetProcessingService) {
155
- const componentInstanceId =
156
- context.componentInstanceId ??
157
- `eco-component-${rapidhash(componentFile)}-${++this.componentRenderSequence}`;
158
- assets = await this.hydrationAssetService.buildComponentRenderAssets(
159
- componentFile,
160
- componentInstanceId,
161
- input.props,
162
- componentConfig,
163
- );
164
- rootAttributes = { 'data-eco-component-id': componentInstanceId };
165
- }
166
-
167
- return {
168
- html,
169
- canAttachAttributes,
170
- rootTag,
171
- integrationName: this.name,
172
- rootAttributes,
173
- assets,
174
- };
175
- }
176
-
177
- /**
178
- * Checks if the given file path corresponds to an MDX file based on configured extensions.
179
- * @param filePath - The file path to check
180
- * @returns True if the file is an MDX file
181
- */
182
- public isMdxFile(filePath: string): boolean {
183
- return this.pageModuleService.isMdxFile(filePath);
184
- }
185
-
186
- /**
187
- * Processes MDX-specific configuration dependencies including layout dependencies.
188
- * @param pagePath - Absolute path to the MDX page file
189
- * @returns Processed assets for MDX configuration dependencies
190
- */
191
- private async processMdxConfigDependencies(pagePath: string): Promise<ProcessedAsset[]> {
192
- const { config } = await this.importPageFile(pagePath);
193
- const resolvedLayout = config?.layout;
194
- const components: Partial<EcoComponent>[] = [];
195
-
196
- if (resolvedLayout?.config?.dependencies) {
197
- const layoutConfig = this.pageModuleService.ensureConfigFileMetadata(resolvedLayout.config, pagePath);
198
- components.push({ config: layoutConfig });
199
- }
200
-
201
- if (config?.dependencies) {
202
- const configWithMeta = {
203
- ...config,
204
- __eco: { id: rapidhash(pagePath).toString(36), file: pagePath, integration: 'react' },
205
- };
206
- components.push({ config: configWithMeta });
207
- }
208
-
209
- return this.processComponentDependencies(components);
210
- }
211
-
212
- override async buildRouteRenderAssets(pagePath: string): Promise<ProcessedAsset[]> {
213
- try {
214
- const pageModule = (await this.importPageFile(pagePath)) as EcoPageFile<{ config?: EcoComponentConfig }> & {
215
- config?: EcoComponentConfig;
216
- };
217
- const shouldHydrate = ReactRenderer.explicitGraphEnabled
218
- ? true
219
- : this.pageModuleService.shouldHydratePage(pageModule);
220
- if (!shouldHydrate) {
221
- return [];
222
- }
223
-
224
- const isMdx = this.pageModuleService.isMdxFile(pagePath);
225
- const declaredModules = this.pageModuleService.collectPageDeclaredModules(pageModule);
226
- const processedAssets = await this.hydrationAssetService.buildRouteRenderAssets(
227
- pagePath,
228
- isMdx,
229
- declaredModules,
230
- );
231
-
232
- if (isMdx) {
233
- const mdxConfigAssets = await this.processMdxConfigDependencies(pagePath);
234
- return [...processedAssets, ...mdxConfigAssets];
235
- }
236
-
237
- return processedAssets;
238
- } catch (error) {
239
- if (error instanceof BundleError) {
240
- console.error('[ecopages] Bundle errors:', error.logs);
241
- }
242
-
243
- throw new ReactRenderError(
244
- `Failed to generate hydration script: ${error instanceof Error ? error.message : String(error)}`,
245
- );
246
- }
247
- }
248
-
249
- protected override async importPageFile(file: string): Promise<EcoPageFile<{ config?: EcoComponentConfig }>> {
250
- const module = (
251
- this.pageModuleService.isMdxFile(file)
252
- ? await this.pageModuleService.importMdxPageFile(file)
253
- : await super.importPageFile(file)
254
- ) as EcoPageFile<{ config?: EcoComponentConfig }> & {
255
- config?: EcoComponentConfig;
256
- };
257
- const { default: Page, getMetadata, config } = module;
258
-
259
- if (this.pageModuleService.isMdxFile(file) && config) {
260
- Page.config = config;
261
- }
262
-
263
- return {
264
- default: Page,
265
- getMetadata,
266
- config,
267
- };
268
- }
269
-
270
- async render({
271
- params,
272
- query,
273
- props,
274
- locals,
275
- pageLocals,
276
- metadata,
277
- Page,
278
- Layout,
279
- HtmlTemplate,
280
- pageProps,
281
- }: IntegrationRendererRenderOptions<ReactNode>): Promise<RouteRendererBody> {
282
- try {
283
- const pageElement = createElement(Page, { params, query, ...props, locals: pageLocals });
284
- const contentElement = Layout
285
- ? createElement(Layout as React.FunctionComponent, { locals } as object, pageElement)
286
- : pageElement;
287
-
288
- const safeLocals = this.getSerializableLocals(locals as RequestLocals);
289
- const allPageProps: HtmlTemplateProps['pageProps'] = {
290
- ...pageProps,
291
- params,
292
- query,
293
- ...(safeLocals && { locals: safeLocals }),
294
- };
295
-
296
- return await renderToReadableStream(
297
- createElement(
298
- HtmlTemplate,
299
- {
300
- metadata,
301
- pageProps: allPageProps,
302
- } as HtmlTemplateProps,
303
- contentElement,
304
- ),
305
- );
306
- } catch (error) {
307
- throw this.createRenderError('Failed to render component', error);
308
- }
309
- }
310
-
311
- /**
312
- * Safely extracts locals for client-side hydration.
313
- *
314
- * On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
315
- * request-scoped data (e.g., session). This data needs to be serialized to the
316
- * client for hydration to match the server-rendered output.
317
- *
318
- * On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
319
- * to prevent accidental use. This method safely detects that case and returns
320
- * `undefined` instead of throwing.
321
- *
322
- * @param locals - The locals object from the render context
323
- * @returns The locals object if serializable, undefined otherwise
324
- */
325
- private getSerializableLocals(locals: RequestLocals): RequestLocals | undefined {
326
- try {
327
- if (locals && Object.keys(locals).length > 0) {
328
- return locals;
329
- }
330
- return undefined;
331
- } catch (e) {
332
- if (e instanceof LocalsAccessError) {
333
- return undefined;
334
- }
335
- throw e;
336
- }
337
- }
338
-
339
- async renderToResponse<P = Record<string, unknown>>(
340
- view: EcoComponent<P>,
341
- props: P,
342
- ctx: RenderToResponseContext,
343
- ): Promise<Response> {
344
- try {
345
- const viewConfig = view.config;
346
- const Layout = viewConfig?.layout as React.FunctionComponent | undefined;
347
-
348
- const ViewComponent = view as unknown as React.FunctionComponent;
349
- const pageElement = createElement(ViewComponent, props || {});
350
-
351
- if (ctx.partial) {
352
- const stream = await renderToReadableStream(pageElement);
353
- return this.createHtmlResponse(stream, ctx);
354
- }
355
-
356
- const contentElement = Layout
357
- ? createElement(Layout as React.FunctionComponent, {}, pageElement)
358
- : pageElement;
359
-
360
- const HtmlTemplate = await this.getHtmlTemplate();
361
- const metadata: PageMetadataProps = view.metadata
362
- ? await view.metadata({
363
- params: {},
364
- query: {},
365
- props,
366
- appConfig: this.appConfig,
367
- })
368
- : this.appConfig.defaultMetadata;
369
-
370
- await this.prepareViewDependencies(view, Layout as unknown as EcoComponent | undefined);
371
-
372
- const viewFilePath = viewConfig?.__eco?.file;
373
- if (viewFilePath) {
374
- const hydrationAssets = await this.buildRouteRenderAssets(viewFilePath);
375
- this.htmlTransformer.setProcessedDependencies([
376
- ...this.htmlTransformer.getProcessedDependencies(),
377
- ...hydrationAssets,
378
- ]);
379
- }
380
-
381
- const streamBody = await renderToReadableStream(
382
- createElement(
383
- HtmlTemplate,
384
- {
385
- metadata,
386
- pageProps: props,
387
- } as HtmlTemplateProps,
388
- contentElement,
389
- ),
390
- );
391
-
392
- const transformedResponse = await this.htmlTransformer.transform(
393
- new Response(streamBody, {
394
- headers: { 'Content-Type': 'text/html' },
395
- }),
396
- );
397
-
398
- return this.createHtmlResponse(transformedResponse.body ?? '', ctx);
399
- } catch (error) {
400
- throw this.createRenderError('Failed to render view', error);
401
- }
402
- }
403
- }
@@ -1,241 +0,0 @@
1
- /**
2
- * This module contains the react plugin for Ecopages
3
- * @module
4
- */
5
- import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
6
- import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
7
- import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
8
- import type { IHmrManager } from '@ecopages/core/internal-types';
9
- import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
10
- import { Logger } from '@ecopages/logger';
11
- import type { CompileOptions } from '@mdx-js/mdx';
12
- import type React from 'react';
13
- import { ReactRenderer } from './react-renderer.ts';
14
- import { ReactHmrStrategy } from './react-hmr-strategy.ts';
15
- import type { ReactRouterAdapter } from './router-adapter.ts';
16
- import type { ComponentBoundaryPolicyInput } from '@ecopages/core/plugins/integration-plugin';
17
- import { ReactRuntimeBundleService } from './services/react-runtime-bundle.service.ts';
18
- import { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.ts';
19
-
20
- const appLogger = new Logger('[ReactPlugin]');
21
-
22
- /**
23
- * MDX configuration options for the React plugin
24
- */
25
- export type ReactMdxOptions = {
26
- /**
27
- * Whether to enable MDX support.
28
- * @default false
29
- */
30
- enabled: boolean;
31
- /**
32
- * Compiler options for MDX.
33
- * @default undefined
34
- */
35
- compilerOptions?: Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
36
- /**
37
- * Remark plugins.
38
- * @default undefined
39
- */
40
- remarkPlugins?: CompileOptions['remarkPlugins'];
41
- /**
42
- * Rehype plugins.
43
- * @default undefined
44
- */
45
- rehypePlugins?: CompileOptions['rehypePlugins'];
46
- /**
47
- * Recma plugins.
48
- * @default undefined
49
- */
50
- recmaPlugins?: CompileOptions['recmaPlugins'];
51
- /**
52
- * Custom extensions to be treated as MDX files.
53
- * @default ['.mdx']
54
- */
55
- extensions?: string[];
56
- };
57
-
58
- /**
59
- * Options for the React plugin
60
- */
61
- export type ReactPluginOptions = {
62
- extensions?: string[];
63
- dependencies?: AssetDefinition[];
64
- /**
65
- * Enables explicit client graph mode for React page entries.
66
- *
67
- * When enabled, React page-entry bundling relies on explicit dependency declarations
68
- * and skips AST-based `middleware`/`requires` stripping in the React path.
69
- * @default false
70
- */
71
- explicitGraph?: boolean;
72
- /**
73
- * Router adapter for SPA navigation.
74
- * When provided, pages with layouts will be wrapped in the router for client-side navigation.
75
- * @example
76
- * ```ts
77
- * import { ecoRouter } from '@ecopages/react-router';
78
- * reactPlugin({ router: ecoRouter() })
79
- * ```
80
- */
81
- router?: ReactRouterAdapter;
82
- /**
83
- * MDX configuration for handling .mdx files within the React plugin.
84
- * When enabled, MDX files are treated as React pages with full router support.
85
- * @example
86
- * ```ts
87
- * reactPlugin({
88
- * router: ecoRouter(),
89
- * mdx: {
90
- * enabled: true,
91
- * extensions: ['.mdx', '.md'],
92
- * remarkPlugins: [remarkGfm],
93
- * rehypePlugins: [[rehypePrettyCode, { theme: '...' }]],
94
- * }
95
- * })
96
- * ```
97
- */
98
- mdx?: ReactMdxOptions;
99
- };
100
-
101
- /**
102
- * The name of the React plugin
103
- */
104
- export const PLUGIN_NAME = 'react';
105
-
106
- /**
107
- * The React plugin class
108
- * This plugin provides support for React components in Ecopages
109
- */
110
- export class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
111
- renderer = ReactRenderer;
112
- routerAdapter: ReactRouterAdapter | undefined;
113
- private mdxEnabled: boolean;
114
- private mdxCompilerOptions?: CompileOptions;
115
- private mdxExtensions: string[];
116
- private mdxLoaderPlugin: EcoBuildPlugin | undefined;
117
- private runtimeBundleService: ReactRuntimeBundleService;
118
- private readonly hmrPageMetadataCache = new ReactHmrPageMetadataCache();
119
- /**
120
- * Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
121
- */
122
- private explicitGraphEnabled: boolean;
123
-
124
- constructor(options?: Omit<ReactPluginOptions, 'name'>) {
125
- const extensions = ['.tsx'];
126
- const mdxExtensions = options?.mdx?.extensions ?? ['.mdx'];
127
-
128
- if (options?.mdx?.enabled) {
129
- extensions.push(...mdxExtensions);
130
- } else if (options?.mdx?.extensions?.length) {
131
- appLogger.warn(
132
- 'MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support.',
133
- );
134
- }
135
-
136
- super({
137
- name: PLUGIN_NAME,
138
- extensions,
139
- ...options,
140
- });
141
-
142
- this.mdxEnabled = options?.mdx?.enabled ?? false;
143
- this.mdxExtensions = mdxExtensions;
144
-
145
- if (this.mdxEnabled) {
146
- const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = options?.mdx || {};
147
- this.mdxCompilerOptions = {
148
- ...compilerOptions,
149
- remarkPlugins: [...(compilerOptions?.remarkPlugins || []), ...(remarkPlugins || [])],
150
- rehypePlugins: [...(compilerOptions?.rehypePlugins || []), ...(rehypePlugins || [])],
151
- recmaPlugins: [...(compilerOptions?.recmaPlugins || []), ...(recmaPlugins || [])],
152
- jsxImportSource: 'react',
153
- jsxRuntime: 'automatic',
154
- development: process.env.NODE_ENV === 'development',
155
- };
156
- appLogger.debug('MDX mode enabled with React jsx runtime');
157
- }
158
-
159
- this.routerAdapter = options?.router;
160
- this.runtimeBundleService = new ReactRuntimeBundleService({
161
- routerAdapter: this.routerAdapter,
162
- });
163
- this.explicitGraphEnabled = options?.explicitGraph ?? false;
164
- ReactRenderer.routerAdapter = this.routerAdapter;
165
- ReactRenderer.mdxCompilerOptions = this.mdxCompilerOptions;
166
- ReactRenderer.mdxExtensions = this.mdxExtensions;
167
- ReactRenderer.explicitGraphEnabled = this.explicitGraphEnabled;
168
- ReactRenderer.hmrPageMetadataCache = this.hmrPageMetadataCache;
169
- this.integrationDependencies.unshift(...this.runtimeBundleService.getDependencies());
170
- }
171
-
172
- override get plugins(): EcoBuildPlugin[] {
173
- if (this.mdxLoaderPlugin) {
174
- return [this.mdxLoaderPlugin];
175
- }
176
- return [];
177
- }
178
-
179
- override async setup(): Promise<void> {
180
- if (this.mdxEnabled && this.mdxCompilerOptions) {
181
- const { createReactMdxLoaderPlugin } = await import('./utils/react-mdx-loader-plugin.ts');
182
- this.mdxLoaderPlugin = createReactMdxLoaderPlugin(this.mdxCompilerOptions);
183
- }
184
- await super.setup();
185
- }
186
-
187
- /**
188
- * Provides React-specific HMR strategy with Fast Refresh support.
189
- *
190
- * The strategy shares a React-only page metadata cache with the renderer so
191
- * save-time rebuilds can reuse declared-module analysis without expanding the
192
- * core HMR interfaces.
193
- *
194
- * @returns ReactHmrStrategy instance for handling React component updates
195
- */
196
- override getHmrStrategy(): HmrStrategy | undefined {
197
- if (!this.hmrManager || !this.appConfig) {
198
- return undefined;
199
- }
200
-
201
- const context = this.hmrManager.getDefaultContext();
202
-
203
- return new ReactHmrStrategy(
204
- context,
205
- this.hmrPageMetadataCache,
206
- this.mdxCompilerOptions,
207
- this.explicitGraphEnabled,
208
- );
209
- }
210
-
211
- /**
212
- * Override to register React-specific specifier mappings for HMR.
213
- */
214
- override setHmrManager(hmrManager: IHmrManager): void {
215
- super.setHmrManager(hmrManager);
216
- hmrManager.registerSpecifierMap(this.runtimeBundleService.getSpecifierMap());
217
- }
218
-
219
- /**
220
- * Declares React's boundary deferral rule for cross-integration rendering.
221
- *
222
- * React defers when a render pass owned by another integration enters a React
223
- * component boundary. That boundary is then resolved later through the marker
224
- * graph stage using the React renderer.
225
- *
226
- * @param input Boundary metadata for the active render pass.
227
- * @returns `true` when the boundary should be deferred into the marker pass.
228
- */
229
- override shouldDeferComponentBoundary(input: ComponentBoundaryPolicyInput): boolean {
230
- return input.targetIntegration === this.name && input.currentIntegration !== this.name;
231
- }
232
- }
233
-
234
- /**
235
- * Factory function to create a React plugin instance
236
- * @param options Configuration options for the React plugin
237
- * @returns A new ReactPlugin instance
238
- */
239
- export function reactPlugin(options?: ReactPluginOptions): ReactPlugin {
240
- return new ReactPlugin(options);
241
- }