@ecopages/react 0.2.0-alpha.5 → 0.2.0-alpha.50
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +152 -29
- package/package.json +16 -12
- package/src/eco-embed.d.ts +11 -0
- package/src/eco-embed.js +11 -0
- package/src/react-hmr-strategy.d.ts +60 -43
- package/src/react-hmr-strategy.js +297 -144
- package/src/react-renderer.d.ts +169 -42
- package/src/react-renderer.js +484 -164
- package/src/react.constants.d.ts +1 -0
- package/src/react.constants.js +4 -0
- package/src/react.plugin.d.ts +38 -111
- package/src/react.plugin.js +132 -61
- package/src/react.types.d.ts +88 -0
- package/src/react.types.js +0 -0
- package/src/router-adapter.d.ts +7 -14
- package/src/services/react-bundle.service.d.ts +19 -31
- package/src/services/react-bundle.service.js +51 -100
- 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 +28 -19
- package/src/services/react-hydration-asset.service.js +85 -66
- 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 +10 -2
- package/src/services/react-page-module.service.js +47 -39
- package/src/services/react-page-payload.service.d.ts +46 -0
- package/src/services/react-page-payload.service.js +67 -0
- package/src/services/react-runtime-bundle.service.d.ts +15 -13
- package/src/services/react-runtime-bundle.service.js +103 -180
- package/src/utils/client-graph-boundary-plugin.d.ts +1 -1
- package/src/utils/client-graph-boundary-plugin.js +80 -3
- package/src/utils/component-config-traversal.d.ts +36 -0
- package/src/utils/component-config-traversal.js +54 -0
- package/src/utils/declared-modules.d.ts +1 -1
- package/src/utils/declared-modules.js +7 -16
- package/src/utils/dynamic.test.browser.d.ts +1 -0
- package/src/utils/dynamic.test.browser.js +33 -0
- package/src/utils/hydration-scripts.d.ts +27 -6
- package/src/utils/hydration-scripts.js +177 -44
- package/src/utils/hydration-scripts.test.browser.d.ts +1 -0
- package/src/utils/hydration-scripts.test.browser.js +198 -0
- package/src/utils/react-dom-runtime-interop-plugin.d.ts +5 -0
- package/src/utils/react-dom-runtime-interop-plugin.js +29 -0
- package/src/utils/react-mdx-loader-plugin.d.ts +1 -1
- package/src/utils/react-mdx-loader-plugin.js +13 -5
- package/src/utils/react-runtime-alias-map.d.ts +6 -0
- package/src/utils/react-runtime-alias-map.js +33 -0
- package/src/utils/use-sync-external-store-shim-plugin.d.ts +5 -0
- package/src/utils/use-sync-external-store-shim-plugin.js +41 -0
- package/CHANGELOG.md +0 -67
- package/src/react-hmr-strategy.ts +0 -455
- package/src/react-renderer.ts +0 -403
- package/src/react.plugin.ts +0 -241
- package/src/router-adapter.ts +0 -95
- package/src/services/react-bundle.service.ts +0 -217
- package/src/services/react-hmr-page-metadata-cache.ts +0 -24
- package/src/services/react-hydration-asset.service.ts +0 -260
- package/src/services/react-page-module.service.ts +0 -214
- package/src/services/react-runtime-bundle.service.ts +0 -271
- package/src/utils/client-graph-boundary-plugin.ts +0 -710
- 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 -338
- package/src/utils/reachability-analyzer.ts +0 -593
- package/src/utils/react-mdx-loader-plugin.ts +0 -40
package/src/utils/client-only.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
|
-
import { useEffect, useState } from 'react';
|
|
3
|
-
|
|
4
|
-
type ClientOnlyProps = {
|
|
5
|
-
children: ReactNode;
|
|
6
|
-
fallback?: ReactNode;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export const useIsClient = (): boolean => {
|
|
10
|
-
const [isClient, setIsClient] = useState(false);
|
|
11
|
-
|
|
12
|
-
useEffect(() => {
|
|
13
|
-
setIsClient(true);
|
|
14
|
-
}, []);
|
|
15
|
-
|
|
16
|
-
return isClient;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export const ClientOnly = ({ children, fallback = null }: ClientOnlyProps): ReactNode => {
|
|
20
|
-
const isClient = useIsClient();
|
|
21
|
-
|
|
22
|
-
if (!isClient) {
|
|
23
|
-
return fallback;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return children;
|
|
27
|
-
};
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities for collecting declared module sources from component configs.
|
|
3
|
-
* Used by both the production ReactRenderer and the HMR strategy to ensure
|
|
4
|
-
* the client-graph-boundary plugin receives a consistent set of allowed modules.
|
|
5
|
-
*
|
|
6
|
-
* @module
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import type { EcoComponentConfig } from '@ecopages/core';
|
|
10
|
-
|
|
11
|
-
type PageConfigModule = {
|
|
12
|
-
default?: { config?: EcoComponentConfig };
|
|
13
|
-
config?: EcoComponentConfig;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Extracts the module source (package name) from a declared module string,
|
|
18
|
-
* stripping any `{namedImport,...}` grammar.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* parseDeclaredModuleSource('@ecopages/image-processor/component/react{EcoImage}')
|
|
22
|
-
* // → '@ecopages/image-processor/component/react'
|
|
23
|
-
*/
|
|
24
|
-
export function parseDeclaredModuleSource(value: string): string | undefined {
|
|
25
|
-
const source = value.trim();
|
|
26
|
-
if (source.length === 0) return undefined;
|
|
27
|
-
const openBraceIndex = source.indexOf('{');
|
|
28
|
-
if (openBraceIndex < 0) return source;
|
|
29
|
-
const from = source.slice(0, openBraceIndex).trim();
|
|
30
|
-
return from.length > 0 ? from : undefined;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Normalizes an array of declared module strings into unique source paths.
|
|
35
|
-
*/
|
|
36
|
-
export function normalizeDeclaredModuleSources(modules?: string[]): string[] {
|
|
37
|
-
const seen = new Set<string>();
|
|
38
|
-
for (const declaration of modules ?? []) {
|
|
39
|
-
const from = parseDeclaredModuleSource(declaration);
|
|
40
|
-
if (from) {
|
|
41
|
-
seen.add(from);
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return Array.from(seen);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Recursively walks a component config tree (including layouts and nested
|
|
49
|
-
* `dependencies.components`) to collect all declared module sources.
|
|
50
|
-
*/
|
|
51
|
-
export function collectDeclaredModulesInConfig(
|
|
52
|
-
config: EcoComponentConfig | undefined,
|
|
53
|
-
visited = new Set<EcoComponentConfig>(),
|
|
54
|
-
): string[] {
|
|
55
|
-
if (!config || visited.has(config)) {
|
|
56
|
-
return [];
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
visited.add(config);
|
|
60
|
-
|
|
61
|
-
const declarations = normalizeDeclaredModuleSources(config.dependencies?.modules);
|
|
62
|
-
|
|
63
|
-
if (config.layout?.config) {
|
|
64
|
-
declarations.push(...collectDeclaredModulesInConfig(config.layout.config, visited));
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
for (const component of config.dependencies?.components ?? []) {
|
|
68
|
-
if (component.config) {
|
|
69
|
-
declarations.push(...collectDeclaredModulesInConfig(component.config, visited));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return declarations;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Collects declared module sources from an already imported page module.
|
|
78
|
-
*/
|
|
79
|
-
export function collectPageDeclaredModulesFromModule(pageModule: PageConfigModule): string[] {
|
|
80
|
-
const declarations = [
|
|
81
|
-
...collectDeclaredModulesInConfig(pageModule.default?.config),
|
|
82
|
-
...collectDeclaredModulesInConfig(pageModule.config),
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
return Array.from(new Set(declarations));
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Imports a page entrypoint and collects all transitively declared module sources
|
|
90
|
-
* from its config, layout config, and nested component configs.
|
|
91
|
-
*/
|
|
92
|
-
export async function collectPageDeclaredModules(pagePath: string): Promise<string[]> {
|
|
93
|
-
try {
|
|
94
|
-
const pageModule = (await import(pagePath)) as PageConfigModule;
|
|
95
|
-
return collectPageDeclaredModulesFromModule(pageModule);
|
|
96
|
-
} catch {
|
|
97
|
-
return [];
|
|
98
|
-
}
|
|
99
|
-
}
|
package/src/utils/dynamic.ts
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { type ComponentType, type LazyExoticComponent, lazy } from 'react';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Dynamically loads a React component with optional SSR support.
|
|
5
|
-
*
|
|
6
|
-
* @param importFn - Function returning a promise that resolves to a React component.
|
|
7
|
-
* @param options - Options for SSR behavior.
|
|
8
|
-
* @returns Lazy loaded component or a null fallback for non-client environments.
|
|
9
|
-
*/
|
|
10
|
-
type DynamicLoaderOptions = {
|
|
11
|
-
ssr?: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const NullComponent: ComponentType = () => null;
|
|
15
|
-
|
|
16
|
-
export function dynamic(
|
|
17
|
-
importFn: () => Promise<{ default: ComponentType<any> }>,
|
|
18
|
-
options: DynamicLoaderOptions = {},
|
|
19
|
-
): LazyExoticComponent<ComponentType<any>> | ComponentType {
|
|
20
|
-
const { ssr = false } = options;
|
|
21
|
-
|
|
22
|
-
if (ssr || typeof window !== 'undefined') {
|
|
23
|
-
return lazy(importFn);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return NullComponent;
|
|
27
|
-
}
|
package/src/utils/hmr-scripts.ts
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* HMR script utilities for React components.
|
|
3
|
-
* @module
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
/** Marker comment to identify already-processed HMR code */
|
|
7
|
-
const HMR_MARKER = '/* [ecopages] react-hmr */';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Checks if code has already been processed with HMR marker.
|
|
11
|
-
* @param code - The bundled code to check
|
|
12
|
-
* @returns True if the code already contains the HMR marker
|
|
13
|
-
*/
|
|
14
|
-
export function hasHmrMarker(code: string): boolean {
|
|
15
|
-
return code.includes(HMR_MARKER);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Injects HMR acceptance handler into bundled code.
|
|
20
|
-
* When a module with React exports changes, it triggers a full invalidation
|
|
21
|
-
* to ensure the parent module re-imports and re-renders with the updated component.
|
|
22
|
-
* @param code - The bundled code to wrap
|
|
23
|
-
* @returns Code with HMR handler injected
|
|
24
|
-
*/
|
|
25
|
-
export function injectHmrHandler(code: string): string {
|
|
26
|
-
return `${HMR_MARKER}
|
|
27
|
-
${code}
|
|
28
|
-
if (import.meta.hot) {
|
|
29
|
-
import.meta.hot.accept((newModule) => {
|
|
30
|
-
if (newModule) {
|
|
31
|
-
const exports = Object.keys(newModule);
|
|
32
|
-
const hasReactExport = exports.some(key => {
|
|
33
|
-
const value = newModule[key];
|
|
34
|
-
return value && (
|
|
35
|
-
typeof value === 'function' ||
|
|
36
|
-
(typeof value === 'object' && value.$$typeof)
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
if (hasReactExport) {
|
|
41
|
-
import.meta.hot.invalidate();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
`;
|
|
47
|
-
}
|
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
const VOID_TAGS = new Set([
|
|
2
|
-
'area',
|
|
3
|
-
'base',
|
|
4
|
-
'br',
|
|
5
|
-
'col',
|
|
6
|
-
'embed',
|
|
7
|
-
'hr',
|
|
8
|
-
'img',
|
|
9
|
-
'input',
|
|
10
|
-
'link',
|
|
11
|
-
'meta',
|
|
12
|
-
'param',
|
|
13
|
-
'source',
|
|
14
|
-
'track',
|
|
15
|
-
'wbr',
|
|
16
|
-
]);
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Returns true when HTML contains exactly one root element node.
|
|
20
|
-
*
|
|
21
|
-
* Used by component-level React rendering to decide whether root attributes can
|
|
22
|
-
* be attached safely without introducing synthetic wrapper nodes.
|
|
23
|
-
*/
|
|
24
|
-
export function hasSingleRootElement(html: string): boolean {
|
|
25
|
-
const firstTagMatch = html.match(/^\s*<([a-zA-Z][a-zA-Z0-9:-]*)\b[^>]*>/);
|
|
26
|
-
if (!firstTagMatch) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const firstTag = firstTagMatch[1].toLowerCase();
|
|
31
|
-
const firstTagText = firstTagMatch[0];
|
|
32
|
-
const firstTagStart = firstTagMatch.index ?? 0;
|
|
33
|
-
const firstTagEnd = firstTagStart + firstTagText.length;
|
|
34
|
-
const isSelfClosing = /\/\s*>$/.test(firstTagText);
|
|
35
|
-
|
|
36
|
-
if (isSelfClosing || VOID_TAGS.has(firstTag)) {
|
|
37
|
-
return html.slice(firstTagEnd).trim().length === 0;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const tokenRegex = /<\/?([a-zA-Z][a-zA-Z0-9:-]*)\b[^>]*>/g;
|
|
41
|
-
tokenRegex.lastIndex = firstTagEnd;
|
|
42
|
-
let depth = 1;
|
|
43
|
-
|
|
44
|
-
for (let token = tokenRegex.exec(html); token; token = tokenRegex.exec(html)) {
|
|
45
|
-
const tagText = token[0];
|
|
46
|
-
const tagName = token[1].toLowerCase();
|
|
47
|
-
const isClosing = tagText.startsWith('</');
|
|
48
|
-
const tokenSelfClosing = /\/\s*>$/.test(tagText);
|
|
49
|
-
|
|
50
|
-
if (VOID_TAGS.has(tagName) || tokenSelfClosing) {
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (isClosing) {
|
|
55
|
-
depth--;
|
|
56
|
-
if (depth === 0) {
|
|
57
|
-
const afterRoot = html.slice(token.index + token[0].length).trim();
|
|
58
|
-
return afterRoot.length === 0;
|
|
59
|
-
}
|
|
60
|
-
} else {
|
|
61
|
-
depth++;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return false;
|
|
66
|
-
}
|
|
@@ -1,338 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hydration script generators for React pages.
|
|
3
|
-
* These functions create the client-side scripts that hydrate React components.
|
|
4
|
-
* @module
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { ReactRouterAdapter } from '../router-adapter.ts';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Options for generating a hydration script.
|
|
11
|
-
*/
|
|
12
|
-
export type HydrationScriptOptions = {
|
|
13
|
-
/** The import path for the bundled page component */
|
|
14
|
-
importPath: string;
|
|
15
|
-
/** Direct import path for React runtime module */
|
|
16
|
-
reactImportPath: string;
|
|
17
|
-
/** Direct import path for react-dom/client runtime module */
|
|
18
|
-
reactDomClientImportPath: string;
|
|
19
|
-
/** Direct import path for router runtime module */
|
|
20
|
-
routerImportPath?: string;
|
|
21
|
-
/** Whether running in development mode with HMR support */
|
|
22
|
-
isDevelopment: boolean;
|
|
23
|
-
/** Whether the source file is an MDX file */
|
|
24
|
-
isMdx: boolean;
|
|
25
|
-
/** Optional router adapter for SPA navigation */
|
|
26
|
-
router?: ReactRouterAdapter;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type IslandHydrationScriptOptions = {
|
|
30
|
-
/** Bundled browser module path for the island component. */
|
|
31
|
-
importPath: string;
|
|
32
|
-
/** Browser import path for React runtime. */
|
|
33
|
-
reactImportPath: string;
|
|
34
|
-
/** Browser import path for react-dom/client runtime. */
|
|
35
|
-
reactDomClientImportPath: string;
|
|
36
|
-
/** Selector that resolves to the SSR root element for this island instance. */
|
|
37
|
-
targetSelector: string;
|
|
38
|
-
/** Serialized component props emitted at render time. */
|
|
39
|
-
props: Record<string, unknown>;
|
|
40
|
-
/** Optional stable component id used to resolve named exports reliably. */
|
|
41
|
-
componentRef?: string;
|
|
42
|
-
/** Optional source file hint used as fallback for component resolution. */
|
|
43
|
-
componentFile?: string;
|
|
44
|
-
/** Enables development-oriented non-minified output. */
|
|
45
|
-
isDevelopment: boolean;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Generates the import statement for the page component.
|
|
50
|
-
* MDX files use namespace imports to access the config export.
|
|
51
|
-
*/
|
|
52
|
-
function getImportStatement(importPath: string, isMdx: boolean): string {
|
|
53
|
-
return isMdx
|
|
54
|
-
? `import * as MDXModule from "${importPath}";\nconst Page = MDXModule.default;\nif (MDXModule.config) Page.config = MDXModule.config;`
|
|
55
|
-
: `import Page from "${importPath}";`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Generates the HMR import statement for hot-reloading.
|
|
60
|
-
* MDX files need to extract config from the new module.
|
|
61
|
-
*/
|
|
62
|
-
function getHmrImportStatement(isMdx: boolean): string {
|
|
63
|
-
return isMdx
|
|
64
|
-
? 'const NewPage = newModule.default; if (newModule.config) NewPage.config = newModule.config;'
|
|
65
|
-
: 'const NewPage = newModule.default;';
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Returns the component type label for logging.
|
|
70
|
-
*/
|
|
71
|
-
function getComponentType(isMdx: boolean): string {
|
|
72
|
-
return isMdx ? 'MDX' : 'React';
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Creates development hydration script with router support.
|
|
77
|
-
* Includes HMR handlers for hot module replacement.
|
|
78
|
-
* Layout is NOT applied here since PageContent handles it.
|
|
79
|
-
*/
|
|
80
|
-
function createDevScriptWithRouter(options: HydrationScriptOptions): string {
|
|
81
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
82
|
-
const { components, getRouterProps } = router!;
|
|
83
|
-
if (!routerImportPath) {
|
|
84
|
-
throw new Error('routerImportPath is required when router adapter is configured');
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return `
|
|
88
|
-
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
89
|
-
import { createElement } from "${reactImportPath}";
|
|
90
|
-
import { ${components.router}, ${components.pageContent} } from "${routerImportPath}";
|
|
91
|
-
${getImportStatement(importPath, isMdx)}
|
|
92
|
-
|
|
93
|
-
window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
|
|
94
|
-
window.__ecopages_router_active__ = false;
|
|
95
|
-
window.__ecopages_reload_current_page__ = null;
|
|
96
|
-
let root = null;
|
|
97
|
-
|
|
98
|
-
const getPageData = () => {
|
|
99
|
-
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
100
|
-
if (el?.textContent) {
|
|
101
|
-
try { return JSON.parse(el.textContent); } catch {}
|
|
102
|
-
}
|
|
103
|
-
return {};
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const props = getPageData();
|
|
107
|
-
|
|
108
|
-
window.__ECO_PAGE__ = {
|
|
109
|
-
module: "${importPath}",
|
|
110
|
-
props
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
const createTree = (Component, props) => {
|
|
114
|
-
const pageContent = createElement(${components.pageContent});
|
|
115
|
-
return createElement(${components.router}, ${getRouterProps('Component', 'props')}, pageContent);
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
const mount = () => {
|
|
119
|
-
root = hydrateRoot(document, createTree(Page, props), {
|
|
120
|
-
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
121
|
-
});
|
|
122
|
-
window.__ecopages_router_active__ = true;
|
|
123
|
-
window.__ecopages_hmr_handlers__["${importPath}"] = async (newUrl) => {
|
|
124
|
-
if (window.__ecopages_router_active__ && window.__ecopages_reload_current_page__) {
|
|
125
|
-
await window.__ecopages_reload_current_page__();
|
|
126
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated via router");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
try {
|
|
130
|
-
const newModule = await import(newUrl);
|
|
131
|
-
${getHmrImportStatement(isMdx)}
|
|
132
|
-
root.render(createTree(NewPage, props));
|
|
133
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
134
|
-
} catch (e) {
|
|
135
|
-
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
if (document.readyState === "loading") {
|
|
141
|
-
document.addEventListener("DOMContentLoaded", mount);
|
|
142
|
-
} else {
|
|
143
|
-
mount();
|
|
144
|
-
}
|
|
145
|
-
`.trim();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Creates development hydration script without router.
|
|
150
|
-
* Includes HMR handlers for hot module replacement.
|
|
151
|
-
*/
|
|
152
|
-
function createDevScriptWithoutRouter(options: HydrationScriptOptions): string {
|
|
153
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
154
|
-
|
|
155
|
-
return `
|
|
156
|
-
import { hydrateRoot } from "${reactDomClientImportPath}";
|
|
157
|
-
import { createElement } from "${reactImportPath}";
|
|
158
|
-
${getImportStatement(importPath, isMdx)}
|
|
159
|
-
|
|
160
|
-
window.__ecopages_hmr_handlers__ = window.__ecopages_hmr_handlers__ || {};
|
|
161
|
-
let root = null;
|
|
162
|
-
|
|
163
|
-
const getPageData = () => {
|
|
164
|
-
const el = document.getElementById("__ECO_PAGE_DATA__");
|
|
165
|
-
if (el?.textContent) {
|
|
166
|
-
try { return JSON.parse(el.textContent); } catch {}
|
|
167
|
-
}
|
|
168
|
-
return {};
|
|
169
|
-
};
|
|
170
|
-
|
|
171
|
-
const props = getPageData();
|
|
172
|
-
|
|
173
|
-
window.__ECO_PAGE__ = {
|
|
174
|
-
module: "${importPath}",
|
|
175
|
-
props
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
const createTree = (Component, props) => {
|
|
179
|
-
const Layout = Component.config?.layout;
|
|
180
|
-
const pageElement = createElement(Component, props);
|
|
181
|
-
return Layout ? createElement(Layout, null, pageElement) : pageElement;
|
|
182
|
-
};
|
|
183
|
-
|
|
184
|
-
const mount = () => {
|
|
185
|
-
root = hydrateRoot(document, createTree(Page, props), {
|
|
186
|
-
onRecoverableError: (err) => console.warn("[ecopages] Hydration error:", err)
|
|
187
|
-
});
|
|
188
|
-
window.__ecopages_hmr_handlers__["${importPath}"] = async (newUrl) => {
|
|
189
|
-
try {
|
|
190
|
-
const newModule = await import(newUrl);
|
|
191
|
-
${getHmrImportStatement(isMdx)}
|
|
192
|
-
root.render(createTree(NewPage, props));
|
|
193
|
-
console.log("[ecopages] ${getComponentType(isMdx)} component updated");
|
|
194
|
-
} catch (e) {
|
|
195
|
-
console.error("[ecopages] Failed to hot-reload ${getComponentType(isMdx)} component:", e);
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
if (document.readyState === "loading") {
|
|
201
|
-
document.addEventListener("DOMContentLoaded", mount);
|
|
202
|
-
} else {
|
|
203
|
-
mount();
|
|
204
|
-
}
|
|
205
|
-
`.trim();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
/**
|
|
209
|
-
* Creates minified production hydration script with router support.
|
|
210
|
-
* Layout is NOT applied here since PageContent handles it.
|
|
211
|
-
*/
|
|
212
|
-
function createProdScriptWithRouter(options: HydrationScriptOptions): string {
|
|
213
|
-
const { importPath, isMdx, router, reactImportPath, reactDomClientImportPath, routerImportPath } = options;
|
|
214
|
-
const { components, getRouterProps } = router!;
|
|
215
|
-
if (!routerImportPath) {
|
|
216
|
-
throw new Error('routerImportPath is required when router adapter is configured');
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (isMdx) {
|
|
220
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGE__={module:"${importPath}",props:pr};const ct=(C,p)=>ce(R,${getRouterProps('C', 'p')},ce(PC));const m=()=>hr(document,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()`;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import{${components.router} as R,${components.pageContent} as PC}from"${routerImportPath}";import P from"${importPath}";const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGE__={module:"${importPath}",props:pr};const ct=(C,p)=>ce(R,${getRouterProps('C', 'p')},ce(PC));const m=()=>hr(document,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()`;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/**
|
|
227
|
-
* Creates minified production hydration script without router.
|
|
228
|
-
*/
|
|
229
|
-
function createProdScriptWithoutRouter(options: HydrationScriptOptions): string {
|
|
230
|
-
const { importPath, isMdx, reactImportPath, reactDomClientImportPath } = options;
|
|
231
|
-
|
|
232
|
-
if (isMdx) {
|
|
233
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import*as M from"${importPath}";const P=M.default;if(M.config)P.config=M.config;const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGE__={module:"${importPath}",props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);return L?ce(L,null,pe):pe};const m=()=>hr(document,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()`;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
return `import{hydrateRoot as hr}from"${reactDomClientImportPath}";import{createElement as ce}from"${reactImportPath}";import P from"${importPath}";const gd=()=>{const e=document.getElementById("__ECO_PAGE_DATA__");if(e?.textContent){try{return JSON.parse(e.textContent)}catch{}}return{}};const pr=gd();window.__ECO_PAGE__={module:"${importPath}",props:pr};const ct=(C,p)=>{const L=C.config?.layout;const pe=ce(C,p);return L?ce(L,null,pe):pe};const m=()=>hr(document,ct(P,pr),{onRecoverableError:(e)=>console.warn("[ecopages] Hydration error:",e)});document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m):m()`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Creates a hydration script for client-side React hydration.
|
|
241
|
-
* Generates appropriate script based on environment and router configuration.
|
|
242
|
-
* @param options - Configuration options for script generation
|
|
243
|
-
* @returns The generated hydration script as a string
|
|
244
|
-
*/
|
|
245
|
-
export function createHydrationScript(options: HydrationScriptOptions): string {
|
|
246
|
-
const { isDevelopment, router } = options;
|
|
247
|
-
|
|
248
|
-
if (isDevelopment) {
|
|
249
|
-
return router ? createDevScriptWithRouter(options) : createDevScriptWithoutRouter(options);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
return router ? createProdScriptWithRouter(options) : createProdScriptWithoutRouter(options);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Creates the client bootstrap for component-level React islands.
|
|
257
|
-
*
|
|
258
|
-
* The island runtime intentionally uses `createRoot()` (not `hydrateRoot()`) and
|
|
259
|
-
* mounts into the SSR element identified by `targetSelector`.
|
|
260
|
-
*
|
|
261
|
-
* Rationale:
|
|
262
|
-
* - No synthetic wrapper element is introduced in SSR output.
|
|
263
|
-
* - DOM structure remains identical to authored component markup.
|
|
264
|
-
* - Runtime ownership is isolated per island instance.
|
|
265
|
-
*
|
|
266
|
-
* Generated script behavior:
|
|
267
|
-
* - resolves the component export by metadata (`componentRef`, `componentFile`)
|
|
268
|
-
* before falling back to default/first function export
|
|
269
|
-
* - selects island root using `targetSelector`
|
|
270
|
-
* - creates a fresh React root and renders with serialized `props`
|
|
271
|
-
*
|
|
272
|
-
* @param options Island script generation options.
|
|
273
|
-
* @returns Browser-executable JavaScript module source.
|
|
274
|
-
*/
|
|
275
|
-
export function createIslandHydrationScript(options: IslandHydrationScriptOptions): string {
|
|
276
|
-
const targetSelector = JSON.stringify(options.targetSelector);
|
|
277
|
-
const serializedProps = JSON.stringify(options.props ?? {});
|
|
278
|
-
const componentRef = JSON.stringify(options.componentRef ?? '');
|
|
279
|
-
const componentFile = JSON.stringify(options.componentFile ?? '');
|
|
280
|
-
const mountedAttribute = 'data-eco-react-mounted';
|
|
281
|
-
|
|
282
|
-
if (options.isDevelopment) {
|
|
283
|
-
return `
|
|
284
|
-
import { createRoot } from "${options.reactDomClientImportPath}";
|
|
285
|
-
import { createElement } from "${options.reactImportPath}";
|
|
286
|
-
import * as ComponentModule from "${options.importPath}";
|
|
287
|
-
|
|
288
|
-
const resolveComponent = () => {
|
|
289
|
-
const id = ${componentRef};
|
|
290
|
-
const file = ${componentFile};
|
|
291
|
-
const moduleValues = Object.values(ComponentModule);
|
|
292
|
-
|
|
293
|
-
const matchByMetadata = moduleValues.find((entry) => {
|
|
294
|
-
if (typeof entry !== "function") return false;
|
|
295
|
-
const config = entry.config;
|
|
296
|
-
const eco = config?.__eco;
|
|
297
|
-
if (!eco) return false;
|
|
298
|
-
if (id && eco.id === id) return true;
|
|
299
|
-
if (file && eco.file === file) return true;
|
|
300
|
-
return false;
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
if (matchByMetadata && typeof matchByMetadata === "function") {
|
|
304
|
-
return matchByMetadata;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const defaultExport = ComponentModule.default;
|
|
308
|
-
if (typeof defaultExport === "function") {
|
|
309
|
-
return defaultExport;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
const firstFunction = moduleValues.find((entry) => typeof entry === "function");
|
|
313
|
-
return typeof firstFunction === "function" ? firstFunction : null;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const mount = () => {
|
|
317
|
-
const target = document.querySelector(${targetSelector});
|
|
318
|
-
const Component = resolveComponent();
|
|
319
|
-
if (!target || !Component || target.hasAttribute("${mountedAttribute}")) {
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
const props = ${serializedProps};
|
|
323
|
-
target.setAttribute("${mountedAttribute}", "true");
|
|
324
|
-
const root = createRoot(target);
|
|
325
|
-
root.render(createElement(Component, props));
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
document.addEventListener("eco:after-swap", mount);
|
|
329
|
-
if (document.readyState === "loading") {
|
|
330
|
-
document.addEventListener("DOMContentLoaded", mount, { once: true });
|
|
331
|
-
} else {
|
|
332
|
-
mount();
|
|
333
|
-
}
|
|
334
|
-
`.trim();
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
return `import{createRoot as cr}from"${options.reactDomClientImportPath}";import{createElement as ce}from"${options.reactImportPath}";import*as M from"${options.importPath}";const r=${componentRef};const f=${componentFile};const mv=Object.values(M);const c=mv.find((e)=>{if(typeof e!=="function")return false;const ec=e.config?.__eco;if(!ec)return false;if(r&&ec.id===r)return true;if(f&&ec.file===f)return true;return false;})??(typeof M.default==="function"?M.default:mv.find((e)=>typeof e==="function")??null);const m=()=>{const t=document.querySelector(${targetSelector});if(!t||!c||t.hasAttribute("${mountedAttribute}"))return;const p=${serializedProps};t.setAttribute("${mountedAttribute}","true");cr(t).render(ce(c,p))};document.addEventListener("eco:after-swap",m);document.readyState==="loading"?document.addEventListener("DOMContentLoaded",m,{once:true}):m()`;
|
|
338
|
-
}
|