@ecopages/react 0.2.0-alpha.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/LICENSE +21 -0
  3. package/README.md +65 -0
  4. package/package.json +76 -0
  5. package/src/declarations.d.ts +6 -0
  6. package/src/react-hmr-strategy.d.ts +143 -0
  7. package/src/react-hmr-strategy.js +332 -0
  8. package/src/react-hmr-strategy.ts +444 -0
  9. package/src/react-renderer.d.ts +106 -0
  10. package/src/react-renderer.js +302 -0
  11. package/src/react-renderer.ts +403 -0
  12. package/src/react.plugin.d.ts +147 -0
  13. package/src/react.plugin.js +126 -0
  14. package/src/react.plugin.ts +241 -0
  15. package/src/router-adapter.d.ts +87 -0
  16. package/src/router-adapter.js +0 -0
  17. package/src/router-adapter.ts +95 -0
  18. package/src/services/react-bundle.service.d.ts +68 -0
  19. package/src/services/react-bundle.service.js +145 -0
  20. package/src/services/react-bundle.service.ts +212 -0
  21. package/src/services/react-hmr-page-metadata-cache.d.ts +17 -0
  22. package/src/services/react-hmr-page-metadata-cache.js +19 -0
  23. package/src/services/react-hmr-page-metadata-cache.ts +24 -0
  24. package/src/services/react-hydration-asset.service.d.ts +75 -0
  25. package/src/services/react-hydration-asset.service.js +198 -0
  26. package/src/services/react-hydration-asset.service.ts +260 -0
  27. package/src/services/react-page-module.service.d.ts +80 -0
  28. package/src/services/react-page-module.service.js +155 -0
  29. package/src/services/react-page-module.service.ts +214 -0
  30. package/src/services/react-runtime-bundle.service.d.ts +38 -0
  31. package/src/services/react-runtime-bundle.service.js +207 -0
  32. package/src/services/react-runtime-bundle.service.ts +271 -0
  33. package/src/utils/client-graph-boundary-plugin.d.ts +43 -0
  34. package/src/utils/client-graph-boundary-plugin.js +356 -0
  35. package/src/utils/client-graph-boundary-plugin.ts +590 -0
  36. package/src/utils/client-only.d.ts +8 -0
  37. package/src/utils/client-only.js +19 -0
  38. package/src/utils/client-only.ts +27 -0
  39. package/src/utils/declared-modules.d.ts +42 -0
  40. package/src/utils/declared-modules.js +56 -0
  41. package/src/utils/declared-modules.ts +99 -0
  42. package/src/utils/dynamic.d.ts +15 -0
  43. package/src/utils/dynamic.js +12 -0
  44. package/src/utils/dynamic.ts +27 -0
  45. package/src/utils/hmr-scripts.d.ts +18 -0
  46. package/src/utils/hmr-scripts.js +31 -0
  47. package/src/utils/hmr-scripts.ts +47 -0
  48. package/src/utils/html-boundary.d.ts +7 -0
  49. package/src/utils/html-boundary.js +55 -0
  50. package/src/utils/html-boundary.ts +66 -0
  51. package/src/utils/hydration-scripts.d.ts +71 -0
  52. package/src/utils/hydration-scripts.js +222 -0
  53. package/src/utils/hydration-scripts.ts +338 -0
  54. package/src/utils/reachability-analyzer.d.ts +55 -0
  55. package/src/utils/reachability-analyzer.js +243 -0
  56. package/src/utils/reachability-analyzer.ts +440 -0
  57. package/src/utils/react-mdx-loader-plugin.d.ts +3 -0
  58. package/src/utils/react-mdx-loader-plugin.js +37 -0
  59. package/src/utils/react-mdx-loader-plugin.ts +40 -0
@@ -0,0 +1,56 @@
1
+ function parseDeclaredModuleSource(value) {
2
+ const source = value.trim();
3
+ if (source.length === 0) return void 0;
4
+ const openBraceIndex = source.indexOf("{");
5
+ if (openBraceIndex < 0) return source;
6
+ const from = source.slice(0, openBraceIndex).trim();
7
+ return from.length > 0 ? from : void 0;
8
+ }
9
+ function normalizeDeclaredModuleSources(modules) {
10
+ const seen = /* @__PURE__ */ new Set();
11
+ for (const declaration of modules ?? []) {
12
+ const from = parseDeclaredModuleSource(declaration);
13
+ if (from) {
14
+ seen.add(from);
15
+ }
16
+ }
17
+ return Array.from(seen);
18
+ }
19
+ function collectDeclaredModulesInConfig(config, visited = /* @__PURE__ */ new Set()) {
20
+ if (!config || visited.has(config)) {
21
+ return [];
22
+ }
23
+ visited.add(config);
24
+ const declarations = normalizeDeclaredModuleSources(config.dependencies?.modules);
25
+ if (config.layout?.config) {
26
+ declarations.push(...collectDeclaredModulesInConfig(config.layout.config, visited));
27
+ }
28
+ for (const component of config.dependencies?.components ?? []) {
29
+ if (component.config) {
30
+ declarations.push(...collectDeclaredModulesInConfig(component.config, visited));
31
+ }
32
+ }
33
+ return declarations;
34
+ }
35
+ function collectPageDeclaredModulesFromModule(pageModule) {
36
+ const declarations = [
37
+ ...collectDeclaredModulesInConfig(pageModule.default?.config),
38
+ ...collectDeclaredModulesInConfig(pageModule.config)
39
+ ];
40
+ return Array.from(new Set(declarations));
41
+ }
42
+ async function collectPageDeclaredModules(pagePath) {
43
+ try {
44
+ const pageModule = await import(pagePath);
45
+ return collectPageDeclaredModulesFromModule(pageModule);
46
+ } catch {
47
+ return [];
48
+ }
49
+ }
50
+ export {
51
+ collectDeclaredModulesInConfig,
52
+ collectPageDeclaredModules,
53
+ collectPageDeclaredModulesFromModule,
54
+ normalizeDeclaredModuleSources,
55
+ parseDeclaredModuleSource
56
+ };
@@ -0,0 +1,99 @@
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
+ }
@@ -0,0 +1,15 @@
1
+ import { type ComponentType, type LazyExoticComponent } from 'react';
2
+ /**
3
+ * Dynamically loads a React component with optional SSR support.
4
+ *
5
+ * @param importFn - Function returning a promise that resolves to a React component.
6
+ * @param options - Options for SSR behavior.
7
+ * @returns Lazy loaded component or a null fallback for non-client environments.
8
+ */
9
+ type DynamicLoaderOptions = {
10
+ ssr?: boolean;
11
+ };
12
+ export declare function dynamic(importFn: () => Promise<{
13
+ default: ComponentType<any>;
14
+ }>, options?: DynamicLoaderOptions): LazyExoticComponent<ComponentType<any>> | ComponentType;
15
+ export {};
@@ -0,0 +1,12 @@
1
+ import { lazy } from "react";
2
+ const NullComponent = () => null;
3
+ function dynamic(importFn, options = {}) {
4
+ const { ssr = false } = options;
5
+ if (ssr || typeof window !== "undefined") {
6
+ return lazy(importFn);
7
+ }
8
+ return NullComponent;
9
+ }
10
+ export {
11
+ dynamic
12
+ };
@@ -0,0 +1,27 @@
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
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * HMR script utilities for React components.
3
+ * @module
4
+ */
5
+ /**
6
+ * Checks if code has already been processed with HMR marker.
7
+ * @param code - The bundled code to check
8
+ * @returns True if the code already contains the HMR marker
9
+ */
10
+ export declare function hasHmrMarker(code: string): boolean;
11
+ /**
12
+ * Injects HMR acceptance handler into bundled code.
13
+ * When a module with React exports changes, it triggers a full invalidation
14
+ * to ensure the parent module re-imports and re-renders with the updated component.
15
+ * @param code - The bundled code to wrap
16
+ * @returns Code with HMR handler injected
17
+ */
18
+ export declare function injectHmrHandler(code: string): string;
@@ -0,0 +1,31 @@
1
+ const HMR_MARKER = "/* [ecopages] react-hmr */";
2
+ function hasHmrMarker(code) {
3
+ return code.includes(HMR_MARKER);
4
+ }
5
+ function injectHmrHandler(code) {
6
+ return `${HMR_MARKER}
7
+ ${code}
8
+ if (import.meta.hot) {
9
+ import.meta.hot.accept((newModule) => {
10
+ if (newModule) {
11
+ const exports = Object.keys(newModule);
12
+ const hasReactExport = exports.some(key => {
13
+ const value = newModule[key];
14
+ return value && (
15
+ typeof value === 'function' ||
16
+ (typeof value === 'object' && value.$$typeof)
17
+ );
18
+ });
19
+
20
+ if (hasReactExport) {
21
+ import.meta.hot.invalidate();
22
+ }
23
+ }
24
+ });
25
+ }
26
+ `;
27
+ }
28
+ export {
29
+ hasHmrMarker,
30
+ injectHmrHandler
31
+ };
@@ -0,0 +1,47 @@
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
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Returns true when HTML contains exactly one root element node.
3
+ *
4
+ * Used by component-level React rendering to decide whether root attributes can
5
+ * be attached safely without introducing synthetic wrapper nodes.
6
+ */
7
+ export declare function hasSingleRootElement(html: string): boolean;
@@ -0,0 +1,55 @@
1
+ const VOID_TAGS = /* @__PURE__ */ 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
+ function hasSingleRootElement(html) {
18
+ const firstTagMatch = html.match(/^\s*<([a-zA-Z][a-zA-Z0-9:-]*)\b[^>]*>/);
19
+ if (!firstTagMatch) {
20
+ return false;
21
+ }
22
+ const firstTag = firstTagMatch[1].toLowerCase();
23
+ const firstTagText = firstTagMatch[0];
24
+ const firstTagStart = firstTagMatch.index ?? 0;
25
+ const firstTagEnd = firstTagStart + firstTagText.length;
26
+ const isSelfClosing = /\/\s*>$/.test(firstTagText);
27
+ if (isSelfClosing || VOID_TAGS.has(firstTag)) {
28
+ return html.slice(firstTagEnd).trim().length === 0;
29
+ }
30
+ const tokenRegex = /<\/?([a-zA-Z][a-zA-Z0-9:-]*)\b[^>]*>/g;
31
+ tokenRegex.lastIndex = firstTagEnd;
32
+ let depth = 1;
33
+ for (let token = tokenRegex.exec(html); token; token = tokenRegex.exec(html)) {
34
+ const tagText = token[0];
35
+ const tagName = token[1].toLowerCase();
36
+ const isClosing = tagText.startsWith("</");
37
+ const tokenSelfClosing = /\/\s*>$/.test(tagText);
38
+ if (VOID_TAGS.has(tagName) || tokenSelfClosing) {
39
+ continue;
40
+ }
41
+ if (isClosing) {
42
+ depth--;
43
+ if (depth === 0) {
44
+ const afterRoot = html.slice(token.index + token[0].length).trim();
45
+ return afterRoot.length === 0;
46
+ }
47
+ } else {
48
+ depth++;
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+ export {
54
+ hasSingleRootElement
55
+ };
@@ -0,0 +1,66 @@
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
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Hydration script generators for React pages.
3
+ * These functions create the client-side scripts that hydrate React components.
4
+ * @module
5
+ */
6
+ import type { ReactRouterAdapter } from '../router-adapter.js';
7
+ /**
8
+ * Options for generating a hydration script.
9
+ */
10
+ export type HydrationScriptOptions = {
11
+ /** The import path for the bundled page component */
12
+ importPath: string;
13
+ /** Direct import path for React runtime module */
14
+ reactImportPath: string;
15
+ /** Direct import path for react-dom/client runtime module */
16
+ reactDomClientImportPath: string;
17
+ /** Direct import path for router runtime module */
18
+ routerImportPath?: string;
19
+ /** Whether running in development mode with HMR support */
20
+ isDevelopment: boolean;
21
+ /** Whether the source file is an MDX file */
22
+ isMdx: boolean;
23
+ /** Optional router adapter for SPA navigation */
24
+ router?: ReactRouterAdapter;
25
+ };
26
+ export type IslandHydrationScriptOptions = {
27
+ /** Bundled browser module path for the island component. */
28
+ importPath: string;
29
+ /** Browser import path for React runtime. */
30
+ reactImportPath: string;
31
+ /** Browser import path for react-dom/client runtime. */
32
+ reactDomClientImportPath: string;
33
+ /** Selector that resolves to the SSR root element for this island instance. */
34
+ targetSelector: string;
35
+ /** Serialized component props emitted at render time. */
36
+ props: Record<string, unknown>;
37
+ /** Optional stable component id used to resolve named exports reliably. */
38
+ componentRef?: string;
39
+ /** Optional source file hint used as fallback for component resolution. */
40
+ componentFile?: string;
41
+ /** Enables development-oriented non-minified output. */
42
+ isDevelopment: boolean;
43
+ };
44
+ /**
45
+ * Creates a hydration script for client-side React hydration.
46
+ * Generates appropriate script based on environment and router configuration.
47
+ * @param options - Configuration options for script generation
48
+ * @returns The generated hydration script as a string
49
+ */
50
+ export declare function createHydrationScript(options: HydrationScriptOptions): string;
51
+ /**
52
+ * Creates the client bootstrap for component-level React islands.
53
+ *
54
+ * The island runtime intentionally uses `createRoot()` (not `hydrateRoot()`) and
55
+ * mounts into the SSR element identified by `targetSelector`.
56
+ *
57
+ * Rationale:
58
+ * - No synthetic wrapper element is introduced in SSR output.
59
+ * - DOM structure remains identical to authored component markup.
60
+ * - Runtime ownership is isolated per island instance.
61
+ *
62
+ * Generated script behavior:
63
+ * - resolves the component export by metadata (`componentRef`, `componentFile`)
64
+ * before falling back to default/first function export
65
+ * - selects island root using `targetSelector`
66
+ * - creates a fresh React root and renders with serialized `props`
67
+ *
68
+ * @param options Island script generation options.
69
+ * @returns Browser-executable JavaScript module source.
70
+ */
71
+ export declare function createIslandHydrationScript(options: IslandHydrationScriptOptions): string;