@ecopages/react 0.2.0-alpha.14 → 0.2.0-alpha.15
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.plugin.d.ts
CHANGED
|
@@ -1,92 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This module contains the react plugin for Ecopages
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
1
|
import { IntegrationPlugin } from '@ecopages/core/plugins/integration-plugin';
|
|
6
2
|
import type { EcoBuildPlugin } from '@ecopages/core/build/build-types';
|
|
7
3
|
import type { HmrStrategy } from '@ecopages/core/hmr/hmr-strategy';
|
|
8
|
-
import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
9
|
-
import type { CompileOptions } from '@mdx-js/mdx';
|
|
10
4
|
import type React from 'react';
|
|
11
5
|
import { ReactRenderer } from './react-renderer.js';
|
|
12
|
-
import type {
|
|
13
|
-
|
|
14
|
-
* MDX configuration options for the React plugin
|
|
15
|
-
*/
|
|
16
|
-
export type ReactMdxOptions = {
|
|
17
|
-
/**
|
|
18
|
-
* Whether to enable MDX support.
|
|
19
|
-
* @default false
|
|
20
|
-
*/
|
|
21
|
-
enabled: boolean;
|
|
22
|
-
/**
|
|
23
|
-
* Compiler options for MDX.
|
|
24
|
-
* @default undefined
|
|
25
|
-
*/
|
|
26
|
-
compilerOptions?: Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
|
|
27
|
-
/**
|
|
28
|
-
* Remark plugins.
|
|
29
|
-
* @default undefined
|
|
30
|
-
*/
|
|
31
|
-
remarkPlugins?: CompileOptions['remarkPlugins'];
|
|
32
|
-
/**
|
|
33
|
-
* Rehype plugins.
|
|
34
|
-
* @default undefined
|
|
35
|
-
*/
|
|
36
|
-
rehypePlugins?: CompileOptions['rehypePlugins'];
|
|
37
|
-
/**
|
|
38
|
-
* Recma plugins.
|
|
39
|
-
* @default undefined
|
|
40
|
-
*/
|
|
41
|
-
recmaPlugins?: CompileOptions['recmaPlugins'];
|
|
42
|
-
/**
|
|
43
|
-
* Custom extensions to be treated as MDX files.
|
|
44
|
-
* @default ['.mdx']
|
|
45
|
-
*/
|
|
46
|
-
extensions?: string[];
|
|
47
|
-
};
|
|
48
|
-
/**
|
|
49
|
-
* Options for the React plugin
|
|
50
|
-
*/
|
|
51
|
-
export type ReactPluginOptions = {
|
|
52
|
-
extensions?: string[];
|
|
53
|
-
dependencies?: AssetDefinition[];
|
|
54
|
-
/**
|
|
55
|
-
* Enables explicit client graph mode for React page entries.
|
|
56
|
-
*
|
|
57
|
-
* When enabled, React page-entry bundling relies on explicit dependency declarations
|
|
58
|
-
* and skips AST-based `middleware`/`requires` stripping in the React path.
|
|
59
|
-
* @default false
|
|
60
|
-
*/
|
|
61
|
-
explicitGraph?: boolean;
|
|
62
|
-
/**
|
|
63
|
-
* Router adapter for SPA navigation.
|
|
64
|
-
* When provided, pages with layouts will be wrapped in the router for client-side navigation.
|
|
65
|
-
* @example
|
|
66
|
-
* ```ts
|
|
67
|
-
* import { ecoRouter } from '@ecopages/react-router';
|
|
68
|
-
* reactPlugin({ router: ecoRouter() })
|
|
69
|
-
* ```
|
|
70
|
-
*/
|
|
71
|
-
router?: ReactRouterAdapter;
|
|
72
|
-
/**
|
|
73
|
-
* MDX configuration for handling .mdx files within the React plugin.
|
|
74
|
-
* When enabled, MDX files are treated as React pages with full router support.
|
|
75
|
-
* @example
|
|
76
|
-
* ```ts
|
|
77
|
-
* reactPlugin({
|
|
78
|
-
* router: ecoRouter(),
|
|
79
|
-
* mdx: {
|
|
80
|
-
* enabled: true,
|
|
81
|
-
* extensions: ['.mdx', '.md'],
|
|
82
|
-
* remarkPlugins: [remarkGfm],
|
|
83
|
-
* rehypePlugins: [[rehypePrettyCode, { theme: '...' }]],
|
|
84
|
-
* }
|
|
85
|
-
* })
|
|
86
|
-
* ```
|
|
87
|
-
*/
|
|
88
|
-
mdx?: ReactMdxOptions;
|
|
89
|
-
};
|
|
6
|
+
import type { ReactPluginOptions } from './react.types.js';
|
|
7
|
+
export type { ReactMdxOptions, ReactPluginOptions, ReactRendererConfig } from './react.types.js';
|
|
90
8
|
/**
|
|
91
9
|
* The name of the React plugin
|
|
92
10
|
*/
|
|
@@ -97,19 +15,30 @@ export declare const PLUGIN_NAME = "react";
|
|
|
97
15
|
*/
|
|
98
16
|
export declare class ReactPlugin extends IntegrationPlugin<React.JSX.Element> {
|
|
99
17
|
renderer: typeof ReactRenderer;
|
|
100
|
-
|
|
101
|
-
private mdxEnabled;
|
|
102
|
-
private mdxCompilerOptions?;
|
|
103
|
-
private mdxExtensions;
|
|
18
|
+
private readonly routerAdapter;
|
|
19
|
+
private readonly mdxEnabled;
|
|
20
|
+
private readonly mdxCompilerOptions?;
|
|
21
|
+
private readonly mdxExtensions;
|
|
104
22
|
private mdxLoaderPlugin;
|
|
105
|
-
private runtimeBundleService;
|
|
23
|
+
private readonly runtimeBundleService;
|
|
106
24
|
private readonly hmrPageMetadataCache;
|
|
107
25
|
private runtimeDependenciesInitialized;
|
|
108
26
|
/**
|
|
109
27
|
* Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
|
|
110
28
|
*/
|
|
111
|
-
private explicitGraphEnabled;
|
|
112
|
-
|
|
29
|
+
private readonly explicitGraphEnabled;
|
|
30
|
+
private readonly rendererConfig;
|
|
31
|
+
constructor(options?: ReactPluginOptions);
|
|
32
|
+
/**
|
|
33
|
+
* Creates a React renderer with instance-owned runtime configuration.
|
|
34
|
+
*
|
|
35
|
+
* React renderers depend on plugin-owned router, MDX, and HMR metadata state.
|
|
36
|
+
* Keeping that state on the instance avoids cross-plugin static mutation while
|
|
37
|
+
* preserving the same runtime services the base initializer wires up.
|
|
38
|
+
*/
|
|
39
|
+
initializeRenderer(options?: {
|
|
40
|
+
rendererModules?: unknown;
|
|
41
|
+
}): ReactRenderer;
|
|
113
42
|
private ensureRuntimeDependencies;
|
|
114
43
|
get plugins(): EcoBuildPlugin[];
|
|
115
44
|
/**
|
package/src/react.plugin.js
CHANGED
|
@@ -6,6 +6,59 @@ import { ReactRuntimeBundleService } from "./services/react-runtime-bundle.servi
|
|
|
6
6
|
import { ReactHmrPageMetadataCache } from "./services/react-hmr-page-metadata-cache.js";
|
|
7
7
|
const appLogger = new Logger("[ReactPlugin]");
|
|
8
8
|
const PLUGIN_NAME = "react";
|
|
9
|
+
const mergePluginLists = (...lists) => {
|
|
10
|
+
const merged = lists.flatMap((list) => list ? [...list] : []);
|
|
11
|
+
return merged.length > 0 ? merged : void 0;
|
|
12
|
+
};
|
|
13
|
+
const appendMdxExtensions = (target, mdxExtensions) => {
|
|
14
|
+
for (const extension of mdxExtensions) {
|
|
15
|
+
if (!target.includes(extension)) {
|
|
16
|
+
target.push(extension);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const resolveReactMdxCompilerOptions = (mdxOptions) => {
|
|
21
|
+
const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = mdxOptions;
|
|
22
|
+
const resolved = {
|
|
23
|
+
...compilerOptions,
|
|
24
|
+
jsxImportSource: "react",
|
|
25
|
+
jsxRuntime: "automatic",
|
|
26
|
+
development: process.env.NODE_ENV === "development"
|
|
27
|
+
};
|
|
28
|
+
const mergedRemark = mergePluginLists(compilerOptions?.remarkPlugins, remarkPlugins);
|
|
29
|
+
const mergedRehype = mergePluginLists(compilerOptions?.rehypePlugins, rehypePlugins);
|
|
30
|
+
const mergedRecma = mergePluginLists(compilerOptions?.recmaPlugins, recmaPlugins);
|
|
31
|
+
if (mergedRemark) resolved.remarkPlugins = mergedRemark;
|
|
32
|
+
if (mergedRehype) resolved.rehypePlugins = mergedRehype;
|
|
33
|
+
if (mergedRecma) resolved.recmaPlugins = mergedRecma;
|
|
34
|
+
return resolved;
|
|
35
|
+
};
|
|
36
|
+
const resolveReactPluginOptions = (options) => {
|
|
37
|
+
const { extensions: userExtensions, router, mdx, explicitGraph, dependencies, ...baseConfig } = options ?? {};
|
|
38
|
+
const extensions = [...userExtensions ?? [".tsx"]];
|
|
39
|
+
const mdxEnabled = mdx?.enabled ?? false;
|
|
40
|
+
const mdxExtensions = mdx?.extensions ?? [".mdx"];
|
|
41
|
+
if (mdxEnabled) {
|
|
42
|
+
appendMdxExtensions(extensions, mdxExtensions);
|
|
43
|
+
} else if (mdx?.extensions?.length) {
|
|
44
|
+
appLogger.warn(
|
|
45
|
+
"MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
const rendererConfig = {
|
|
49
|
+
routerAdapter: router,
|
|
50
|
+
mdxCompilerOptions: mdxEnabled && mdx ? resolveReactMdxCompilerOptions(mdx) : void 0,
|
|
51
|
+
mdxExtensions,
|
|
52
|
+
hmrPageMetadataCache: new ReactHmrPageMetadataCache(),
|
|
53
|
+
explicitGraphEnabled: explicitGraph ?? false
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
...baseConfig,
|
|
57
|
+
extensions,
|
|
58
|
+
integrationDependencies: dependencies,
|
|
59
|
+
rendererConfig
|
|
60
|
+
};
|
|
61
|
+
};
|
|
9
62
|
class ReactPlugin extends IntegrationPlugin {
|
|
10
63
|
renderer = ReactRenderer;
|
|
11
64
|
routerAdapter;
|
|
@@ -14,58 +67,55 @@ class ReactPlugin extends IntegrationPlugin {
|
|
|
14
67
|
mdxExtensions;
|
|
15
68
|
mdxLoaderPlugin;
|
|
16
69
|
runtimeBundleService;
|
|
17
|
-
hmrPageMetadataCache
|
|
70
|
+
hmrPageMetadataCache;
|
|
18
71
|
runtimeDependenciesInitialized = false;
|
|
19
72
|
/**
|
|
20
73
|
* Indicates whether React explicit graph mode is enabled for renderer/HMR behavior.
|
|
21
74
|
*/
|
|
22
75
|
explicitGraphEnabled;
|
|
76
|
+
rendererConfig;
|
|
23
77
|
constructor(options) {
|
|
24
|
-
const
|
|
25
|
-
const extensions
|
|
26
|
-
const mdxExtensions = options?.mdx?.extensions ?? [".mdx"];
|
|
27
|
-
if (options?.mdx?.enabled) {
|
|
28
|
-
for (const extension of mdxExtensions) {
|
|
29
|
-
if (!extensions.includes(extension)) {
|
|
30
|
-
extensions.push(extension);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
} else if (options?.mdx?.extensions?.length) {
|
|
34
|
-
appLogger.warn(
|
|
35
|
-
"MDX extensions provided but MDX is disabled. MDX files will not be processed. Set mdx.enabled to true to enable MDX support."
|
|
36
|
-
);
|
|
37
|
-
}
|
|
78
|
+
const config = resolveReactPluginOptions(options);
|
|
79
|
+
const { extensions, rendererConfig, integrationDependencies, ...baseConfig } = config;
|
|
38
80
|
super({
|
|
39
81
|
name: PLUGIN_NAME,
|
|
40
82
|
extensions,
|
|
41
83
|
jsxImportSource: "react",
|
|
42
|
-
|
|
84
|
+
integrationDependencies,
|
|
85
|
+
...baseConfig
|
|
43
86
|
});
|
|
44
|
-
this.
|
|
45
|
-
this.
|
|
87
|
+
this.routerAdapter = rendererConfig.routerAdapter;
|
|
88
|
+
this.mdxCompilerOptions = rendererConfig.mdxCompilerOptions;
|
|
89
|
+
this.mdxEnabled = Boolean(rendererConfig.mdxCompilerOptions);
|
|
90
|
+
this.mdxExtensions = rendererConfig.mdxExtensions ?? [".mdx"];
|
|
91
|
+
this.hmrPageMetadataCache = rendererConfig.hmrPageMetadataCache ?? new ReactHmrPageMetadataCache();
|
|
92
|
+
this.explicitGraphEnabled = rendererConfig.explicitGraphEnabled ?? false;
|
|
93
|
+
this.rendererConfig = {
|
|
94
|
+
...rendererConfig,
|
|
95
|
+
mdxExtensions: this.mdxExtensions,
|
|
96
|
+
hmrPageMetadataCache: this.hmrPageMetadataCache,
|
|
97
|
+
explicitGraphEnabled: this.explicitGraphEnabled
|
|
98
|
+
};
|
|
46
99
|
if (this.mdxEnabled) {
|
|
47
|
-
const { compilerOptions, remarkPlugins, rehypePlugins, recmaPlugins } = options?.mdx || {};
|
|
48
|
-
this.mdxCompilerOptions = {
|
|
49
|
-
...compilerOptions,
|
|
50
|
-
remarkPlugins: [...compilerOptions?.remarkPlugins || [], ...remarkPlugins || []],
|
|
51
|
-
rehypePlugins: [...compilerOptions?.rehypePlugins || [], ...rehypePlugins || []],
|
|
52
|
-
recmaPlugins: [...compilerOptions?.recmaPlugins || [], ...recmaPlugins || []],
|
|
53
|
-
jsxImportSource: "react",
|
|
54
|
-
jsxRuntime: "automatic",
|
|
55
|
-
development: process.env.NODE_ENV === "development"
|
|
56
|
-
};
|
|
57
100
|
appLogger.debug("MDX mode enabled with React jsx runtime");
|
|
58
101
|
}
|
|
59
|
-
this.routerAdapter = options?.router;
|
|
60
102
|
this.runtimeBundleService = new ReactRuntimeBundleService({
|
|
61
103
|
routerAdapter: this.routerAdapter
|
|
62
104
|
});
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Creates a React renderer with instance-owned runtime configuration.
|
|
108
|
+
*
|
|
109
|
+
* React renderers depend on plugin-owned router, MDX, and HMR metadata state.
|
|
110
|
+
* Keeping that state on the instance avoids cross-plugin static mutation while
|
|
111
|
+
* preserving the same runtime services the base initializer wires up.
|
|
112
|
+
*/
|
|
113
|
+
initializeRenderer(options) {
|
|
114
|
+
const renderer = new this.renderer({
|
|
115
|
+
...this.createRendererOptions(options),
|
|
116
|
+
reactConfig: this.rendererConfig
|
|
117
|
+
});
|
|
118
|
+
return this.attachRendererRuntimeServices(renderer);
|
|
69
119
|
}
|
|
70
120
|
ensureRuntimeDependencies() {
|
|
71
121
|
if (this.runtimeDependenciesInitialized) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { AssetDefinition } from '@ecopages/core/services/asset-processing-service';
|
|
2
|
+
import type { CompileOptions } from '@mdx-js/mdx';
|
|
3
|
+
import type { ReactRouterAdapter } from './router-adapter.js';
|
|
4
|
+
import type { ReactHmrPageMetadataCache } from './services/react-hmr-page-metadata-cache.js';
|
|
5
|
+
/**
|
|
6
|
+
* MDX configuration options for the React plugin.
|
|
7
|
+
*/
|
|
8
|
+
export type ReactMdxOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Whether to enable MDX support.
|
|
11
|
+
* @default false
|
|
12
|
+
*/
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Compiler options for MDX.
|
|
16
|
+
* @default undefined
|
|
17
|
+
*/
|
|
18
|
+
compilerOptions?: Omit<CompileOptions, 'jsxImportSource' | 'jsxRuntime'>;
|
|
19
|
+
/**
|
|
20
|
+
* Remark plugins.
|
|
21
|
+
* @default undefined
|
|
22
|
+
*/
|
|
23
|
+
remarkPlugins?: CompileOptions['remarkPlugins'];
|
|
24
|
+
/**
|
|
25
|
+
* Rehype plugins.
|
|
26
|
+
* @default undefined
|
|
27
|
+
*/
|
|
28
|
+
rehypePlugins?: CompileOptions['rehypePlugins'];
|
|
29
|
+
/**
|
|
30
|
+
* Recma plugins.
|
|
31
|
+
* @default undefined
|
|
32
|
+
*/
|
|
33
|
+
recmaPlugins?: CompileOptions['recmaPlugins'];
|
|
34
|
+
/**
|
|
35
|
+
* Custom extensions to be treated as MDX files.
|
|
36
|
+
* @default ['.mdx']
|
|
37
|
+
*/
|
|
38
|
+
extensions?: string[];
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Options for the React plugin.
|
|
42
|
+
*/
|
|
43
|
+
export type ReactPluginOptions = {
|
|
44
|
+
extensions?: string[];
|
|
45
|
+
dependencies?: AssetDefinition[];
|
|
46
|
+
/**
|
|
47
|
+
* Enables explicit client graph mode for React page entries.
|
|
48
|
+
*
|
|
49
|
+
* When enabled, React page-entry bundling relies on explicit dependency declarations
|
|
50
|
+
* and skips AST-based `middleware`/`requires` stripping in the React path.
|
|
51
|
+
* @default false
|
|
52
|
+
*/
|
|
53
|
+
explicitGraph?: boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Router adapter for SPA navigation.
|
|
56
|
+
* When provided, pages with layouts will be wrapped in the router for client-side navigation.
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* import { ecoRouter } from '@ecopages/react-router';
|
|
60
|
+
* reactPlugin({ router: ecoRouter() })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
router?: ReactRouterAdapter;
|
|
64
|
+
/**
|
|
65
|
+
* MDX configuration for handling .mdx files within the React plugin.
|
|
66
|
+
* When enabled, MDX files are treated as React pages with full router support.
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* reactPlugin({
|
|
70
|
+
* router: ecoRouter(),
|
|
71
|
+
* mdx: {
|
|
72
|
+
* enabled: true,
|
|
73
|
+
* extensions: ['.mdx', '.md'],
|
|
74
|
+
* remarkPlugins: [remarkGfm],
|
|
75
|
+
* rehypePlugins: [[rehypePrettyCode, { theme: '...' }]],
|
|
76
|
+
* }
|
|
77
|
+
* })
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
mdx?: ReactMdxOptions;
|
|
81
|
+
};
|
|
82
|
+
export type ReactRendererConfig = {
|
|
83
|
+
routerAdapter?: ReactRouterAdapter;
|
|
84
|
+
mdxCompilerOptions?: CompileOptions;
|
|
85
|
+
mdxExtensions?: string[];
|
|
86
|
+
hmrPageMetadataCache?: ReactHmrPageMetadataCache;
|
|
87
|
+
explicitGraphEnabled?: boolean;
|
|
88
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EcoComponent, EcoComponentConfig } from '@ecopages/core';
|
|
2
|
+
import { type AssetProcessingService, type ProcessedAsset } from '@ecopages/core/services/asset-processing-service';
|
|
3
|
+
import type { ReactPageModuleService } from './react-page-module.service.js';
|
|
4
|
+
type MdxConfigDependencyProcessor = (components: Partial<EcoComponent>[]) => Promise<ProcessedAsset[]>;
|
|
5
|
+
export interface ReactMdxConfigDependencyServiceConfig {
|
|
6
|
+
integrationName: string;
|
|
7
|
+
pageModuleService: Pick<ReactPageModuleService, 'ensureConfigFileMetadata'>;
|
|
8
|
+
assetProcessingService?: Pick<AssetProcessingService, 'processDependencies'>;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves MDX-owned config dependencies that live outside the normal React component tree.
|
|
12
|
+
*
|
|
13
|
+
* React MDX pages can declare dependencies on the page config itself or on a
|
|
14
|
+
* resolved layout config. Those roots need to be materialized as synthetic
|
|
15
|
+
* component configs so the shared dependency pipeline can process them without
|
|
16
|
+
* growing more MDX-specific logic inside the renderer.
|
|
17
|
+
*/
|
|
18
|
+
export declare class ReactMdxConfigDependencyService {
|
|
19
|
+
private readonly config;
|
|
20
|
+
constructor(config: ReactMdxConfigDependencyServiceConfig);
|
|
21
|
+
/**
|
|
22
|
+
* Processes MDX-owned config dependencies and eagerly emits any SSR-marked lazy scripts.
|
|
23
|
+
*/
|
|
24
|
+
processMdxConfigDependencies(options: {
|
|
25
|
+
pagePath: string;
|
|
26
|
+
config?: EcoComponentConfig;
|
|
27
|
+
processComponentDependencies: MdxConfigDependencyProcessor;
|
|
28
|
+
}): Promise<ProcessedAsset[]>;
|
|
29
|
+
private createOwnedConfigComponents;
|
|
30
|
+
private processDeclaredSsrLazyDependencies;
|
|
31
|
+
/**
|
|
32
|
+
* Collects `lazy` script dependencies that also opt into SSR from an MDX config graph.
|
|
33
|
+
*/
|
|
34
|
+
private collectDeclaredSsrLazyDependencies;
|
|
35
|
+
}
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { rapidhash } from "@ecopages/core/hash";
|
|
3
|
+
import {
|
|
4
|
+
AssetFactory
|
|
5
|
+
} from "@ecopages/core/services/asset-processing-service";
|
|
6
|
+
import { collectFromConfigForest, getComponentConfigs } from "../utils/component-config-traversal.js";
|
|
7
|
+
class ReactMdxConfigDependencyService {
|
|
8
|
+
config;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Processes MDX-owned config dependencies and eagerly emits any SSR-marked lazy scripts.
|
|
14
|
+
*/
|
|
15
|
+
async processMdxConfigDependencies(options) {
|
|
16
|
+
const components = this.createOwnedConfigComponents(options.pagePath, options.config);
|
|
17
|
+
if (components.length === 0) {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
const processedDependencies = await options.processComponentDependencies(components);
|
|
21
|
+
const eagerSsrLazyDependencies = await this.processDeclaredSsrLazyDependencies(components, options.pagePath);
|
|
22
|
+
return [...processedDependencies, ...eagerSsrLazyDependencies];
|
|
23
|
+
}
|
|
24
|
+
createOwnedConfigComponents(pagePath, config) {
|
|
25
|
+
const components = [];
|
|
26
|
+
const resolvedLayout = config?.layout;
|
|
27
|
+
if (resolvedLayout?.config?.dependencies) {
|
|
28
|
+
const layoutConfig = this.config.pageModuleService.ensureConfigFileMetadata(
|
|
29
|
+
resolvedLayout.config,
|
|
30
|
+
pagePath
|
|
31
|
+
);
|
|
32
|
+
components.push({ config: layoutConfig });
|
|
33
|
+
}
|
|
34
|
+
if (config?.dependencies) {
|
|
35
|
+
components.push({
|
|
36
|
+
config: {
|
|
37
|
+
...config,
|
|
38
|
+
__eco: {
|
|
39
|
+
id: rapidhash(pagePath).toString(36),
|
|
40
|
+
file: pagePath,
|
|
41
|
+
integration: this.config.integrationName
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return components;
|
|
47
|
+
}
|
|
48
|
+
async processDeclaredSsrLazyDependencies(components, pagePath) {
|
|
49
|
+
if (!this.config.assetProcessingService?.processDependencies) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const dependencies = this.collectDeclaredSsrLazyDependencies(components);
|
|
53
|
+
if (dependencies.length === 0) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
return this.config.assetProcessingService.processDependencies(
|
|
57
|
+
dependencies,
|
|
58
|
+
`${this.config.integrationName}-mdx-ssr-lazy:${pagePath}`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Collects `lazy` script dependencies that also opt into SSR from an MDX config graph.
|
|
63
|
+
*/
|
|
64
|
+
collectDeclaredSsrLazyDependencies(components) {
|
|
65
|
+
const dependencies = [];
|
|
66
|
+
const seenKeys = /* @__PURE__ */ new Set();
|
|
67
|
+
const normalizeAttributes = (attributes) => ({
|
|
68
|
+
type: "module",
|
|
69
|
+
defer: "",
|
|
70
|
+
...attributes ?? {}
|
|
71
|
+
});
|
|
72
|
+
collectFromConfigForest(getComponentConfigs(components), (config) => {
|
|
73
|
+
const componentFile = config.__eco?.file;
|
|
74
|
+
if (!componentFile) {
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
const componentDir = path.dirname(componentFile);
|
|
78
|
+
for (const script of config.dependencies?.scripts ?? []) {
|
|
79
|
+
if (typeof script === "string" || !script.lazy || script.ssr !== true) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const attributes = normalizeAttributes(script.attributes);
|
|
83
|
+
if (script.content) {
|
|
84
|
+
const key2 = `content:${script.content}:${JSON.stringify(attributes)}`;
|
|
85
|
+
if (seenKeys.has(key2)) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
seenKeys.add(key2);
|
|
89
|
+
dependencies.push(
|
|
90
|
+
AssetFactory.createContentScript({
|
|
91
|
+
position: "head",
|
|
92
|
+
content: script.content,
|
|
93
|
+
attributes
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!script.src) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const resolvedPath = path.resolve(componentDir, script.src);
|
|
102
|
+
const key = `file:${resolvedPath}:${JSON.stringify(attributes)}`;
|
|
103
|
+
if (seenKeys.has(key)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
seenKeys.add(key);
|
|
107
|
+
dependencies.push(
|
|
108
|
+
AssetFactory.createFileScript({
|
|
109
|
+
filepath: resolvedPath,
|
|
110
|
+
position: "head",
|
|
111
|
+
attributes
|
|
112
|
+
})
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
});
|
|
117
|
+
return dependencies;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
export {
|
|
121
|
+
ReactMdxConfigDependencyService
|
|
122
|
+
};
|
|
@@ -43,7 +43,10 @@ export declare class ReactPageModuleService {
|
|
|
43
43
|
* @param filePath - Absolute path to the MDX file
|
|
44
44
|
* @returns The imported module
|
|
45
45
|
*/
|
|
46
|
-
importMdxPageFile(filePath: string
|
|
46
|
+
importMdxPageFile(filePath: string, options?: {
|
|
47
|
+
bypassCache?: boolean;
|
|
48
|
+
cacheScope?: string;
|
|
49
|
+
}): Promise<unknown>;
|
|
47
50
|
/**
|
|
48
51
|
* Ensures that an EcoComponentConfig has proper `__eco` metadata attached.
|
|
49
52
|
* Resolves the file path from dependency declarations when not already set.
|
|
@@ -57,7 +60,7 @@ export declare class ReactPageModuleService {
|
|
|
57
60
|
* Recursively checks whether a component config tree declares any browser modules.
|
|
58
61
|
* Used to determine if a page needs hydration.
|
|
59
62
|
*/
|
|
60
|
-
hasModulesInConfig(config: EcoComponentConfig | undefined
|
|
63
|
+
hasModulesInConfig(config: EcoComponentConfig | undefined): boolean;
|
|
61
64
|
/**
|
|
62
65
|
* Determines whether a page needs client-side hydration.
|
|
63
66
|
*
|
|
@@ -3,6 +3,7 @@ import { pathToFileURL } from "node:url";
|
|
|
3
3
|
import { rapidhash } from "@ecopages/core/hash";
|
|
4
4
|
import { build } from "@ecopages/core/build/build-adapter";
|
|
5
5
|
import { fileSystem } from "@ecopages/file-system";
|
|
6
|
+
import { someInConfigTree } from "../utils/component-config-traversal.js";
|
|
6
7
|
import { collectDeclaredModulesInConfig } from "../utils/declared-modules.js";
|
|
7
8
|
class ReactPageModuleService {
|
|
8
9
|
config;
|
|
@@ -23,7 +24,7 @@ class ReactPageModuleService {
|
|
|
23
24
|
* @param filePath - Absolute path to the MDX file
|
|
24
25
|
* @returns The imported module
|
|
25
26
|
*/
|
|
26
|
-
async importMdxPageFile(filePath) {
|
|
27
|
+
async importMdxPageFile(filePath, options) {
|
|
27
28
|
const { createReactMdxLoaderPlugin } = await import("../utils/react-mdx-loader-plugin.js");
|
|
28
29
|
const mdxPlugin = createReactMdxLoaderPlugin(
|
|
29
30
|
this.config.mdxCompilerOptions ?? {
|
|
@@ -35,8 +36,9 @@ class ReactPageModuleService {
|
|
|
35
36
|
const outdir = path.join(this.config.workDir, ".server-modules-react-mdx");
|
|
36
37
|
const fileBaseName = path.basename(filePath, path.extname(filePath));
|
|
37
38
|
const fileHash = fileSystem.hash(filePath);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
39
|
+
const cacheScopeSuffix = options?.cacheScope ? `-${sanitizeCacheScope(options.cacheScope)}` : "";
|
|
40
|
+
const cacheBuster = options?.bypassCache || process?.env?.NODE_ENV === "development" ? `-${Date.now()}` : "";
|
|
41
|
+
const outputFileName = `${fileBaseName}-${fileHash}${cacheScopeSuffix}${cacheBuster}.js`;
|
|
40
42
|
const buildResult = await build(
|
|
41
43
|
{
|
|
42
44
|
entrypoints: [filePath],
|
|
@@ -62,9 +64,16 @@ class ReactPageModuleService {
|
|
|
62
64
|
if (!compiledOutput) {
|
|
63
65
|
throw new Error(`No compiled MDX output generated for page: ${filePath}`);
|
|
64
66
|
}
|
|
67
|
+
const compiledOutputUrl = pathToFileURL(compiledOutput);
|
|
68
|
+
if (process?.env?.NODE_ENV === "development" || options?.cacheScope) {
|
|
69
|
+
compiledOutputUrl.searchParams.set(
|
|
70
|
+
"update",
|
|
71
|
+
[fileHash, options?.cacheScope ? sanitizeCacheScope(options.cacheScope) : void 0].filter((value) => value !== void 0).join("-")
|
|
72
|
+
);
|
|
73
|
+
}
|
|
65
74
|
return await import(
|
|
66
75
|
/* @vite-ignore */
|
|
67
|
-
|
|
76
|
+
compiledOutputUrl.href
|
|
68
77
|
);
|
|
69
78
|
}
|
|
70
79
|
/**
|
|
@@ -112,23 +121,11 @@ class ReactPageModuleService {
|
|
|
112
121
|
* Recursively checks whether a component config tree declares any browser modules.
|
|
113
122
|
* Used to determine if a page needs hydration.
|
|
114
123
|
*/
|
|
115
|
-
hasModulesInConfig(config
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (config.dependencies?.modules?.some((entry) => entry.trim().length > 0)) {
|
|
121
|
-
return true;
|
|
122
|
-
}
|
|
123
|
-
if (config.layout?.config && this.hasModulesInConfig(config.layout.config, visited)) {
|
|
124
|
-
return true;
|
|
125
|
-
}
|
|
126
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
127
|
-
if (this.hasModulesInConfig(component.config, visited)) {
|
|
128
|
-
return true;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
124
|
+
hasModulesInConfig(config) {
|
|
125
|
+
return someInConfigTree(
|
|
126
|
+
config,
|
|
127
|
+
(node) => node.dependencies?.modules?.some((entry) => entry.trim().length > 0) ?? false
|
|
128
|
+
);
|
|
132
129
|
}
|
|
133
130
|
/**
|
|
134
131
|
* Determines whether a page needs client-side hydration.
|
|
@@ -157,6 +154,9 @@ class ReactPageModuleService {
|
|
|
157
154
|
return Array.from(new Set(declarations));
|
|
158
155
|
}
|
|
159
156
|
}
|
|
157
|
+
function sanitizeCacheScope(cacheScope) {
|
|
158
|
+
return cacheScope.replace(/[^a-zA-Z0-9_-]+/g, "-");
|
|
159
|
+
}
|
|
160
160
|
export {
|
|
161
161
|
ReactPageModuleService
|
|
162
162
|
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { HtmlTemplateProps, IntegrationRendererRenderOptions, RequestLocals } from '@ecopages/core';
|
|
2
|
+
import type { ReactNode } from 'react';
|
|
3
|
+
type PagePayloadOptions = {
|
|
4
|
+
pageProps?: HtmlTemplateProps['pageProps'];
|
|
5
|
+
params: IntegrationRendererRenderOptions<ReactNode>['params'];
|
|
6
|
+
query: IntegrationRendererRenderOptions<ReactNode>['query'];
|
|
7
|
+
safeLocals?: RequestLocals;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Builds the serialized page payload React exposes to document shells and the browser.
|
|
11
|
+
*
|
|
12
|
+
* This keeps hydration payload shaping away from the renderer so the renderer can
|
|
13
|
+
* stay focused on component orchestration instead of data serialization rules.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ReactPagePayloadService {
|
|
16
|
+
/**
|
|
17
|
+
* Creates the canonical page-props payload used by router hydration.
|
|
18
|
+
*
|
|
19
|
+
* React pages embedded in a non-React HTML shell still need to expose the same
|
|
20
|
+
* page-data contract as fully React-owned documents so navigation and hydration
|
|
21
|
+
* can read one shared document payload consistently.
|
|
22
|
+
*/
|
|
23
|
+
buildRouterPageDataScript(pageProps: HtmlTemplateProps['pageProps'] | undefined): string;
|
|
24
|
+
/**
|
|
25
|
+
* Builds the serialized page-props payload embedded into the final HTML.
|
|
26
|
+
*
|
|
27
|
+
* The document payload is intentionally narrower than the full server render
|
|
28
|
+
* input: only routing data, public page props, and explicitly allowed locals are
|
|
29
|
+
* exposed to the browser.
|
|
30
|
+
*/
|
|
31
|
+
buildSerializedPageProps(options: PagePayloadOptions): HtmlTemplateProps['pageProps'];
|
|
32
|
+
/**
|
|
33
|
+
* Safely extracts the declared subset of locals for client-side hydration.
|
|
34
|
+
*
|
|
35
|
+
* On dynamic pages with `cache: 'dynamic'`, middleware populates `locals` with
|
|
36
|
+
* request-scoped data (e.g., session). Only keys explicitly declared via
|
|
37
|
+
* `Page.requires` are serialized to the client so sensitive request-only data
|
|
38
|
+
* is not leaked into hydration payloads by default.
|
|
39
|
+
*
|
|
40
|
+
* On static pages, `locals` is a Proxy that throws `LocalsAccessError` on access
|
|
41
|
+
* to prevent accidental use. This method safely detects that case and returns
|
|
42
|
+
* `undefined` instead of throwing.
|
|
43
|
+
*/
|
|
44
|
+
getSerializableLocals(locals: RequestLocals | undefined, requiredLocals?: string | readonly string[]): RequestLocals | undefined;
|
|
45
|
+
}
|
|
46
|
+
export {};
|