@harpy-js/core 0.4.7

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 (147) hide show
  1. package/README.md +326 -0
  2. package/dist/cli.d.ts +12 -0
  3. package/dist/cli.js +53 -0
  4. package/dist/client/Link.d.ts +5 -0
  5. package/dist/client/Link.js +62 -0
  6. package/dist/client/__tests__/getActiveItemId.test.d.ts +1 -0
  7. package/dist/client/__tests__/getActiveItemId.test.js +38 -0
  8. package/dist/client/getActiveItemId.d.ts +7 -0
  9. package/dist/client/getActiveItemId.js +55 -0
  10. package/dist/client/use-i18n.d.ts +7 -0
  11. package/dist/client/use-i18n.js +64 -0
  12. package/dist/core/__tests__/component-analyzer.test.d.ts +1 -0
  13. package/dist/core/__tests__/component-analyzer.test.js +151 -0
  14. package/dist/core/__tests__/hydration-manifest.test.d.ts +1 -0
  15. package/dist/core/__tests__/hydration-manifest.test.js +211 -0
  16. package/dist/core/__tests__/jsx.engine.test.d.ts +1 -0
  17. package/dist/core/__tests__/jsx.engine.test.js +118 -0
  18. package/dist/core/app-setup.d.ts +7 -0
  19. package/dist/core/app-setup.js +79 -0
  20. package/dist/core/auto-register.module.d.ts +9 -0
  21. package/dist/core/auto-register.module.js +18 -0
  22. package/dist/core/auto-wrap-middleware.d.ts +4 -0
  23. package/dist/core/auto-wrap-middleware.js +130 -0
  24. package/dist/core/client-component-wrapper.d.ts +5 -0
  25. package/dist/core/client-component-wrapper.js +37 -0
  26. package/dist/core/client-hydration.d.ts +2 -0
  27. package/dist/core/client-hydration.js +93 -0
  28. package/dist/core/client-wrapper-browser.d.ts +2 -0
  29. package/dist/core/client-wrapper-browser.js +22 -0
  30. package/dist/core/component-analyzer.d.ts +4 -0
  31. package/dist/core/component-analyzer.js +98 -0
  32. package/dist/core/component-auto-wrapper.d.ts +2 -0
  33. package/dist/core/component-auto-wrapper.js +63 -0
  34. package/dist/core/component-client-wrapper.d.ts +4 -0
  35. package/dist/core/component-client-wrapper.js +80 -0
  36. package/dist/core/hydration-generator.d.ts +2 -0
  37. package/dist/core/hydration-generator.js +98 -0
  38. package/dist/core/hydration-manifest.d.ts +7 -0
  39. package/dist/core/hydration-manifest.js +83 -0
  40. package/dist/core/hydration.d.ts +16 -0
  41. package/dist/core/hydration.js +72 -0
  42. package/dist/core/jsx.engine.d.ts +9 -0
  43. package/dist/core/jsx.engine.js +161 -0
  44. package/dist/core/live-reload-client.js +32 -0
  45. package/dist/core/live-reload.controller.d.ts +10 -0
  46. package/dist/core/live-reload.controller.js +38 -0
  47. package/dist/core/navigation.service.d.ts +18 -0
  48. package/dist/core/navigation.service.js +206 -0
  49. package/dist/core/router.module.d.ts +2 -0
  50. package/dist/core/router.module.js +21 -0
  51. package/dist/core/static-assets.controller.d.ts +4 -0
  52. package/dist/core/static-assets.controller.js +51 -0
  53. package/dist/core/types/nav.types.d.ts +22 -0
  54. package/dist/core/types/nav.types.js +2 -0
  55. package/dist/core/views/layout.d.ts +8 -0
  56. package/dist/core/views/layout.js +35 -0
  57. package/dist/decorators/jsx.decorator.d.ts +26 -0
  58. package/dist/decorators/jsx.decorator.js +10 -0
  59. package/dist/decorators/layout.decorator.d.ts +4 -0
  60. package/dist/decorators/layout.decorator.js +29 -0
  61. package/dist/i18n/__tests__/i18n.helper.test.d.ts +1 -0
  62. package/dist/i18n/__tests__/i18n.helper.test.js +105 -0
  63. package/dist/i18n/__tests__/i18n.interceptor.test.d.ts +1 -0
  64. package/dist/i18n/__tests__/i18n.interceptor.test.js +195 -0
  65. package/dist/i18n/__tests__/i18n.module.test.d.ts +1 -0
  66. package/dist/i18n/__tests__/i18n.module.test.js +83 -0
  67. package/dist/i18n/__tests__/i18n.service.test.d.ts +1 -0
  68. package/dist/i18n/__tests__/i18n.service.test.js +109 -0
  69. package/dist/i18n/__tests__/t.test.d.ts +1 -0
  70. package/dist/i18n/__tests__/t.test.js +66 -0
  71. package/dist/i18n/i18n-module.options.d.ts +10 -0
  72. package/dist/i18n/i18n-module.options.js +4 -0
  73. package/dist/i18n/i18n-switcher.controller.d.ts +12 -0
  74. package/dist/i18n/i18n-switcher.controller.js +80 -0
  75. package/dist/i18n/i18n-types.d.ts +8 -0
  76. package/dist/i18n/i18n-types.js +2 -0
  77. package/dist/i18n/i18n.helper.d.ts +14 -0
  78. package/dist/i18n/i18n.helper.js +70 -0
  79. package/dist/i18n/i18n.interceptor.d.ts +9 -0
  80. package/dist/i18n/i18n.interceptor.js +99 -0
  81. package/dist/i18n/i18n.module.d.ts +5 -0
  82. package/dist/i18n/i18n.module.js +51 -0
  83. package/dist/i18n/i18n.service.d.ts +12 -0
  84. package/dist/i18n/i18n.service.js +61 -0
  85. package/dist/i18n/index.d.ts +10 -0
  86. package/dist/i18n/index.js +20 -0
  87. package/dist/i18n/locale.decorator.d.ts +1 -0
  88. package/dist/i18n/locale.decorator.js +8 -0
  89. package/dist/i18n/t.d.ts +3 -0
  90. package/dist/i18n/t.js +16 -0
  91. package/dist/index.d.ts +19 -0
  92. package/dist/index.js +40 -0
  93. package/package.json +79 -0
  94. package/scripts/analyze-styles.ts +124 -0
  95. package/scripts/auto-wrap-exports.ts +239 -0
  96. package/scripts/build-css.ts +38 -0
  97. package/scripts/build-hydration.ts +313 -0
  98. package/scripts/build-page-styles.ts +43 -0
  99. package/scripts/copy-assets.ts +34 -0
  100. package/scripts/dev.sh +3 -0
  101. package/scripts/dev.ts +257 -0
  102. package/src/cli.ts +71 -0
  103. package/src/client/Link.tsx +62 -0
  104. package/src/client/__tests__/getActiveItemId.test.ts +49 -0
  105. package/src/client/getActiveItemId.ts +54 -0
  106. package/src/client/use-i18n.ts +111 -0
  107. package/src/core/__tests__/component-analyzer.test.ts +141 -0
  108. package/src/core/__tests__/hydration-manifest.test.ts +223 -0
  109. package/src/core/__tests__/jsx.engine.test.ts +137 -0
  110. package/src/core/app-setup.ts +114 -0
  111. package/src/core/auto-register.module.ts +30 -0
  112. package/src/core/auto-wrap-middleware.ts +165 -0
  113. package/src/core/client-component-wrapper.ts +72 -0
  114. package/src/core/client-hydration.tsx +99 -0
  115. package/src/core/client-wrapper-browser.ts +40 -0
  116. package/src/core/component-analyzer.ts +89 -0
  117. package/src/core/component-auto-wrapper.ts +68 -0
  118. package/src/core/component-client-wrapper.ts +112 -0
  119. package/src/core/hydration-generator.ts +94 -0
  120. package/src/core/hydration-manifest.ts +79 -0
  121. package/src/core/hydration.ts +70 -0
  122. package/src/core/jsx.engine.ts +205 -0
  123. package/src/core/live-reload-client.js +32 -0
  124. package/src/core/live-reload.controller.ts +55 -0
  125. package/src/core/navigation.service.ts +257 -0
  126. package/src/core/router.module.ts +9 -0
  127. package/src/core/static-assets.controller.ts +19 -0
  128. package/src/core/types/nav.types.ts +53 -0
  129. package/src/core/views/layout.tsx +61 -0
  130. package/src/decorators/jsx.decorator.ts +49 -0
  131. package/src/decorators/layout.decorator.ts +66 -0
  132. package/src/i18n/__tests__/i18n.helper.test.ts +126 -0
  133. package/src/i18n/__tests__/i18n.interceptor.test.ts +229 -0
  134. package/src/i18n/__tests__/i18n.module.test.ts +98 -0
  135. package/src/i18n/__tests__/i18n.service.test.ts +129 -0
  136. package/src/i18n/__tests__/t.test.ts +88 -0
  137. package/src/i18n/i18n-module.options.ts +53 -0
  138. package/src/i18n/i18n-switcher.controller.ts +99 -0
  139. package/src/i18n/i18n-types.ts +56 -0
  140. package/src/i18n/i18n.helper.ts +75 -0
  141. package/src/i18n/i18n.interceptor.ts +114 -0
  142. package/src/i18n/i18n.module.ts +45 -0
  143. package/src/i18n/i18n.service.ts +95 -0
  144. package/src/i18n/index.ts +37 -0
  145. package/src/i18n/locale.decorator.ts +10 -0
  146. package/src/i18n/t.ts +62 -0
  147. package/src/index.ts +31 -0
@@ -0,0 +1,72 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Generates a unique instance ID (browser-compatible version)
5
+ */
6
+ function generateInstanceId(prefix: string): string {
7
+ // Use Math.random for browser compatibility
8
+ const randomId = Math.random().toString(36).substring(2, 11);
9
+ return `${prefix}-${randomId}`;
10
+ }
11
+
12
+ /**
13
+ * Global tracker for registering components on the server side
14
+ * This is set by the JSX engine during server-side rendering
15
+ */
16
+ declare global {
17
+ var __COMPONENT_REGISTRY__: ((data: any) => void) | undefined;
18
+ }
19
+
20
+ /**
21
+ * Wrapper that enables automatic hydration for components marked with 'use client'.
22
+ *
23
+ * This works on both server and browser:
24
+ * - Server: Creates a hydration container with props and calls the registration function if available
25
+ * - Browser: Creates the same hydration container for client-side hydration
26
+ */
27
+ export function autoWrapClientComponent<T extends React.ComponentType<any>>(
28
+ Component: T,
29
+ componentName: string,
30
+ ): React.ComponentType<React.ComponentProps<T>> {
31
+ const WrappedComponent = (props: React.ComponentProps<T>) => {
32
+ // Generate a unique instance ID for this component instance
33
+ const instanceId = generateInstanceId(`hydrate-${componentName}`);
34
+
35
+ // Register on server side if registry is available
36
+ console.log(
37
+ `[Wrapper] Rendering ${componentName}, registry available:`,
38
+ typeof global !== "undefined" && !!global.__COMPONENT_REGISTRY__,
39
+ );
40
+ if (typeof global !== "undefined" && global.__COMPONENT_REGISTRY__) {
41
+ console.log(
42
+ `[Wrapper] Registering ${componentName} with id ${instanceId}`,
43
+ );
44
+ global.__COMPONENT_REGISTRY__({
45
+ componentPath: "",
46
+ componentName,
47
+ instanceId,
48
+ props: props as Record<string, any>,
49
+ });
50
+ }
51
+
52
+ // Store props in a script tag for client-side hydration access
53
+ const propsJson = JSON.stringify(props);
54
+
55
+ return React.createElement(
56
+ "div",
57
+ {
58
+ id: instanceId,
59
+ suppressHydrationWarning: true,
60
+ },
61
+ React.createElement(Component, props),
62
+ React.createElement("script", {
63
+ type: "application/json",
64
+ id: `${instanceId}-props`,
65
+ dangerouslySetInnerHTML: { __html: propsJson },
66
+ }),
67
+ );
68
+ };
69
+
70
+ WrappedComponent.displayName = `ClientComponent(${componentName})`;
71
+ return WrappedComponent;
72
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Client-side hydration entry point.
3
+ * This script is injected into the HTML and runs in the browser to hydrate interactive components.
4
+ */
5
+
6
+ import React from "react";
7
+
8
+ /**
9
+ * Client-side hydration entry point.
10
+ * This script is injected into the HTML and runs in the browser to hydrate interactive components.
11
+ */
12
+
13
+ interface HydrationMarker {
14
+ componentName: string;
15
+ instanceId: string;
16
+ propsId: string;
17
+ }
18
+
19
+ /**
20
+ * Finds all hydration markers in the DOM and returns their data
21
+ */
22
+ function findHydrationMarkers(): HydrationMarker[] {
23
+ const markers: HydrationMarker[] = [];
24
+
25
+ // Find all divs with id starting with 'hydrate-'
26
+ const hydrationDivs = document.querySelectorAll('div[id^="hydrate-"]');
27
+
28
+ hydrationDivs.forEach((div) => {
29
+ const instanceId = div.id;
30
+ // Parse instanceId: "hydrate-ComponentName-uniqueid"
31
+ const match = instanceId.match(/^hydrate-([^-]+)-(.+)$/);
32
+
33
+ if (match) {
34
+ const componentName = match[1];
35
+ const propsScriptId = `${instanceId}-props`;
36
+
37
+ markers.push({
38
+ componentName,
39
+ instanceId,
40
+ propsId: propsScriptId,
41
+ });
42
+ }
43
+ });
44
+
45
+ return markers;
46
+ }
47
+
48
+ /**
49
+ * Gets props for a hydration marker from the embedded script tag
50
+ */
51
+ function getHydrationProps(propsId: string): Record<string, any> {
52
+ const propsScript = document.getElementById(propsId);
53
+ if (!propsScript) return {};
54
+
55
+ try {
56
+ return JSON.parse(propsScript.textContent || "{}");
57
+ } catch (e) {
58
+ console.warn(`Failed to parse props for ${propsId}:`, e);
59
+ return {};
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Hydrates a single client component
65
+ * This is called by the bundled component chunk scripts
66
+ */
67
+ export function hydrateComponent(
68
+ componentName: string,
69
+ Component: React.ComponentType<any>,
70
+ ) {
71
+ const markers = findHydrationMarkers();
72
+ const relevantMarkers = markers.filter(
73
+ (m) => m.componentName === componentName,
74
+ );
75
+
76
+ relevantMarkers.forEach((marker) => {
77
+ const container = document.getElementById(marker.instanceId);
78
+ if (!container) {
79
+ console.warn(`Hydration container not found for ${marker.instanceId}`);
80
+ return;
81
+ }
82
+
83
+ const props = getHydrationProps(marker.propsId);
84
+
85
+ // Dynamically import React for hydration
86
+ Promise.all([import("react-dom/client")]).then(([{ hydrateRoot }]) => {
87
+ try {
88
+ hydrateRoot(container, React.createElement(Component, props));
89
+ } catch (error) {
90
+ console.error(`Failed to hydrate component ${componentName}:`, error);
91
+ }
92
+ });
93
+ });
94
+ }
95
+
96
+ // Export for use in component bundles
97
+ if (typeof window !== "undefined") {
98
+ (window as any).__HYDRATION__ = { hydrateComponent };
99
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Browser-safe version of client component wrapper
3
+ * This version is used only for client-side hydration and doesn't import Node modules
4
+ */
5
+
6
+ import React from "react";
7
+
8
+ /**
9
+ * Browser-compatible instance ID generator
10
+ */
11
+ function generateBrowserInstanceId(prefix: string): string {
12
+ const randomId = Math.random().toString(36).substring(2, 11);
13
+ return `${prefix}-${randomId}`;
14
+ }
15
+
16
+ /**
17
+ * Client-side wrapper for hydration
18
+ * Used during client-side hydration to wrap components
19
+ */
20
+ export function hydrateClientComponent<T extends React.ComponentType<any>>(
21
+ Component: T,
22
+ componentName: string,
23
+ props: Record<string, any>,
24
+ ): React.ReactElement {
25
+ const instanceId = generateBrowserInstanceId(`hydrate-${componentName}`);
26
+
27
+ return React.createElement(
28
+ "div",
29
+ {
30
+ id: instanceId,
31
+ suppressHydrationWarning: true,
32
+ },
33
+ React.createElement(Component, props),
34
+ React.createElement("script", {
35
+ type: "application/json",
36
+ id: `${instanceId}-props`,
37
+ dangerouslySetInnerHTML: { __html: JSON.stringify(props) },
38
+ }),
39
+ );
40
+ }
@@ -0,0 +1,89 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ /**
5
+ * Detects if a component file has the 'use client' directive
6
+ */
7
+ export function hasUseClientDirective(filePath: string): boolean {
8
+ try {
9
+ const content = fs.readFileSync(filePath, "utf-8");
10
+ // Match "use client" directive at the start of the file, possibly after comments
11
+ const useClientRegex = /^(['"]use client['"];?\s*)/m;
12
+ return useClientRegex.test(content);
13
+ } catch (error) {
14
+ console.error(`Error reading file ${filePath}:`, error);
15
+ return false;
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Get the component name from file path
21
+ * Example: /src/features/home/views/counter.tsx -> Counter (converted from counter)
22
+ */
23
+ export function getComponentNameFromPath(filePath: string): string {
24
+ const fileName = path.basename(filePath, path.extname(filePath));
25
+ // Convert kebab-case to PascalCase
26
+ return fileName
27
+ .split("-")
28
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
29
+ .join("");
30
+ }
31
+
32
+ /**
33
+ * Recursively find all component files (.tsx/.ts) in a directory
34
+ */
35
+ export function findComponentFiles(
36
+ dir: string,
37
+ exclude: string[] = [],
38
+ ): string[] {
39
+ const components: string[] = [];
40
+
41
+ function traverse(currentPath: string) {
42
+ try {
43
+ const files = fs.readdirSync(currentPath);
44
+
45
+ for (const file of files) {
46
+ const fullPath = path.join(currentPath, file);
47
+ const stat = fs.statSync(fullPath);
48
+
49
+ // Skip excluded paths
50
+ if (exclude.some((ex) => fullPath.includes(ex))) {
51
+ continue;
52
+ }
53
+
54
+ if (stat.isDirectory()) {
55
+ traverse(fullPath);
56
+ } else if (file.endsWith(".tsx") || file.endsWith(".ts")) {
57
+ // Only include files that export components (convention: in views/ or end with Component)
58
+ if (
59
+ fullPath.includes("/views/") ||
60
+ file.endsWith(".component.ts") ||
61
+ file.endsWith(".component.tsx")
62
+ ) {
63
+ components.push(fullPath);
64
+ }
65
+ }
66
+ }
67
+ } catch (error) {
68
+ console.error(`Error traversing directory ${currentPath}:`, error);
69
+ }
70
+ }
71
+
72
+ traverse(dir);
73
+ return components;
74
+ }
75
+
76
+ /**
77
+ * Get client components from source directory
78
+ */
79
+ export function getClientComponents(srcDir: string): string[] {
80
+ const components = findComponentFiles(srcDir, [
81
+ "/node_modules/",
82
+ "/dist/",
83
+ "/test/",
84
+ "/core/", // Exclude internal framework code
85
+ ]);
86
+ return components.filter((componentPath) =>
87
+ hasUseClientDirective(componentPath),
88
+ );
89
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Auto-wrapping system for components with 'use client' directive.
3
+ *
4
+ * This module provides utilities to automatically detect and wrap client components
5
+ * without requiring manual withClientComponent() calls in application code.
6
+ *
7
+ * The wrapping happens transparently at the component definition level.
8
+ */
9
+
10
+ import * as fs from "fs";
11
+ import React from "react";
12
+ import { autoWrapClientComponent } from "./client-component-wrapper";
13
+ import { getComponentNameFromPath } from "./component-analyzer";
14
+
15
+ /**
16
+ * Cache of file paths and whether they have 'use client' directive
17
+ * Key: absolute file path, Value: boolean indicating presence of 'use client'
18
+ */
19
+ const clientComponentCache = new Map<string, boolean>();
20
+
21
+ /**
22
+ * Detects if a source file has 'use client' directive
23
+ */
24
+ function hasUseClientDirective(filePath: string): boolean {
25
+ if (clientComponentCache.has(filePath)) {
26
+ return clientComponentCache.get(filePath)!;
27
+ }
28
+
29
+ try {
30
+ const content = fs.readFileSync(filePath, "utf-8");
31
+ // Match "use client" at the very start of the file
32
+ const hasDirective = /^(['"]use client['"];?\s*)/m.test(content);
33
+ clientComponentCache.set(filePath, hasDirective);
34
+ return hasDirective;
35
+ } catch (error) {
36
+ clientComponentCache.set(filePath, false);
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Wraps a component if its source file contains 'use client' directive.
43
+ * This is the recommended way to use client components - just export them normally
44
+ * with 'use client' at the top, and they'll be auto-wrapped.
45
+ *
46
+ * Usage:
47
+ * export default autoWrap(MyComponent, import.meta.url);
48
+ *
49
+ * Or better yet, use the custom hook approach in component-client.ts
50
+ */
51
+ export function autoWrap<T extends React.ComponentType<any>>(
52
+ Component: T,
53
+ fileUrl: string,
54
+ ): T {
55
+ // Convert URL to file path
56
+ const filePath = fileUrl.replace("file://", "");
57
+
58
+ // If this component file doesn't have 'use client', return it unwrapped
59
+ if (!hasUseClientDirective(filePath)) {
60
+ return Component;
61
+ }
62
+
63
+ // Get component name from file path
64
+ const componentName = getComponentNameFromPath(filePath);
65
+
66
+ // Wrap for auto-hydration
67
+ return autoWrapClientComponent(Component, componentName) as T;
68
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Smart component export wrapper that automatically detects 'use client' directive
3
+ * and applies hydration wrapping without requiring manual withClientComponent() calls.
4
+ *
5
+ * This simulates Next.js's automatic client component handling.
6
+ */
7
+
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ import React from "react";
11
+ import { autoWrapClientComponent } from "./client-component-wrapper";
12
+ import { getComponentNameFromPath } from "./component-analyzer";
13
+
14
+ /**
15
+ * Cache to avoid re-reading files repeatedly
16
+ */
17
+ const componentCache = new Map<
18
+ string,
19
+ {
20
+ isClientComponent: boolean;
21
+ wrappedComponent: React.ComponentType<any> | null;
22
+ }
23
+ >();
24
+
25
+ /**
26
+ * Check if a file has 'use client' directive by reading the source
27
+ */
28
+ function detectClientComponent(sourceFilePath: string): {
29
+ isClientComponent: boolean;
30
+ componentPath: string;
31
+ } {
32
+ // Normalize the path
33
+ const normalizedPath = path.resolve(sourceFilePath);
34
+
35
+ if (componentCache.has(normalizedPath)) {
36
+ const cached = componentCache.get(normalizedPath)!;
37
+ return {
38
+ isClientComponent: cached.isClientComponent,
39
+ componentPath: normalizedPath,
40
+ };
41
+ }
42
+
43
+ let isClientComponent = false;
44
+
45
+ try {
46
+ const content = fs.readFileSync(normalizedPath, "utf-8");
47
+ isClientComponent = /^['"]use client['"];?\s*/.test(content);
48
+ } catch (error) {
49
+ // File doesn't exist or can't be read - treat as server component
50
+ isClientComponent = false;
51
+ }
52
+
53
+ componentCache.set(normalizedPath, {
54
+ isClientComponent,
55
+ wrappedComponent: null,
56
+ });
57
+
58
+ return { isClientComponent, componentPath: normalizedPath };
59
+ }
60
+
61
+ /**
62
+ * Higher-order component that automatically wraps client components.
63
+ *
64
+ * Usage in component files:
65
+ * ```tsx
66
+ * 'use client';
67
+ *
68
+ * function MyComponent() { ... }
69
+ * export default createClientComponent(MyComponent, __filename);
70
+ * ```
71
+ *
72
+ * The __filename reference will be resolved at build time or runtime.
73
+ * This function detects the 'use client' directive and wraps accordingly.
74
+ */
75
+ export function createClientComponent<T extends React.ComponentType<any>>(
76
+ Component: T,
77
+ sourceFile: string,
78
+ ): T {
79
+ const { isClientComponent, componentPath } =
80
+ detectClientComponent(sourceFile);
81
+
82
+ if (!isClientComponent) {
83
+ // Not a client component, return as-is
84
+ return Component;
85
+ }
86
+
87
+ // Get the component name from the file path
88
+ const componentName = getComponentNameFromPath(componentPath);
89
+
90
+ // Wrap for automatic hydration
91
+ return autoWrapClientComponent(Component, componentName) as T;
92
+ }
93
+
94
+ /**
95
+ * Alternative: Automatically wrap a component based on its __filename
96
+ *
97
+ * Can be used as a default export wrapper:
98
+ * export default autoWrapIfClient(MyComponent, __filename);
99
+ */
100
+ export function autoWrapIfClient<T extends React.ComponentType<any>>(
101
+ Component: T,
102
+ filename: string,
103
+ ): T {
104
+ return createClientComponent(Component, filename);
105
+ }
106
+
107
+ /**
108
+ * Clear the component cache (useful for testing or hot reload)
109
+ */
110
+ export function clearComponentCache(): void {
111
+ componentCache.clear();
112
+ }
@@ -0,0 +1,94 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import {
4
+ getClientComponents,
5
+ getComponentNameFromPath,
6
+ } from "./component-analyzer";
7
+
8
+ /**
9
+ * Generates separate entry point files for each client component
10
+ * These files will be bundled separately to reduce bundle size
11
+ */
12
+ export function generateHydrationEntryFiles(
13
+ srcDir: string,
14
+ outputDir: string,
15
+ ): string[] {
16
+ const clientComponentPaths = getClientComponents(srcDir);
17
+
18
+ // Ensure output directory exists
19
+ if (!fs.existsSync(outputDir)) {
20
+ fs.mkdirSync(outputDir, { recursive: true });
21
+ }
22
+
23
+ const entryFiles: string[] = [];
24
+
25
+ for (const componentPath of clientComponentPaths) {
26
+ const componentName = getComponentNameFromPath(componentPath);
27
+ const relativePath = path.relative(srcDir, componentPath);
28
+
29
+ // Compute relative import path from assets/hydrate to the component
30
+ const fromAssets = path.join(outputDir, "dummy.ts");
31
+ let importPath = path.relative(path.dirname(fromAssets), componentPath);
32
+
33
+ // Normalize path separators to forward slashes (required for imports)
34
+ importPath = importPath.replace(/\\/g, "/");
35
+
36
+ // Remove .tsx/.ts extension if present and add it explicitly
37
+ importPath = importPath.replace(/\.tsx?$/, "");
38
+
39
+ // Create entry file for this component
40
+ const entryFileName = `${componentName}.ts`;
41
+ const entryFilePath = path.join(outputDir, entryFileName);
42
+
43
+ // Generate the entry file content
44
+ const entryContent = `
45
+ import { hydrateRoot } from 'react-dom/client';
46
+ import * as React from 'react';
47
+
48
+ // Import the client component
49
+ import ${componentName} from '${importPath}';
50
+
51
+ /**
52
+ * Auto-generated hydration entry for ${componentName}
53
+ * This file is bundled separately to reduce the main bundle size
54
+ */
55
+
56
+ // Find all instances of this component marked with data-hydration-id
57
+ const instances = document.querySelectorAll('[data-hydration-id]');
58
+
59
+ instances.forEach((element) => {
60
+ const hydrationId = element.getAttribute('data-hydration-id');
61
+ if (hydrationId) {
62
+ try {
63
+ // Hydrate the component
64
+ hydrateRoot(element, React.createElement(${componentName}));
65
+ console.log('[Hydration] Successfully hydrated ${componentName} (id: ' + hydrationId + ')');
66
+ } catch (error) {
67
+ console.error('[Hydration] Failed to hydrate ${componentName}:', error);
68
+ }
69
+ }
70
+ });
71
+ `.trim();
72
+
73
+ fs.writeFileSync(entryFilePath, entryContent, "utf-8");
74
+ entryFiles.push(entryFilePath);
75
+
76
+ console.log(`Generated hydration entry: ${entryFileName}`);
77
+ }
78
+
79
+ return entryFiles;
80
+ }
81
+
82
+ /**
83
+ * Get all hydration entry files that were generated
84
+ */
85
+ export function getHydrationEntryFiles(outputDir: string): string[] {
86
+ if (!fs.existsSync(outputDir)) {
87
+ return [];
88
+ }
89
+
90
+ return fs
91
+ .readdirSync(outputDir)
92
+ .filter((file) => file.endsWith(".ts") && !file.endsWith(".spec.ts"))
93
+ .map((file) => path.join(outputDir, file));
94
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Hydration manifest loader
3
+ * Reads the generated hydration manifest at runtime to map component names to chunked filenames
4
+ */
5
+
6
+ import * as fs from "fs";
7
+ import * as path from "path";
8
+
9
+ export interface HydrationManifest {
10
+ [componentName: string]: string; // componentName -> chunkFileName
11
+ }
12
+
13
+ let cachedManifest: HydrationManifest | null = null;
14
+
15
+ /**
16
+ * Get the hydration manifest
17
+ * The manifest is generated during build and contains mappings of component names to cache-busted chunk filenames
18
+ */
19
+ export function getHydrationManifest(): HydrationManifest {
20
+ if (cachedManifest) {
21
+ return cachedManifest;
22
+ }
23
+
24
+ // Try dist folder first (where it's generated), then legacy src location
25
+ const manifestPaths = [
26
+ path.join(process.cwd(), "dist", "hydration-manifest.json"),
27
+ path.join(process.cwd(), "src", "hydration-manifest.json"), // legacy fallback
28
+ ];
29
+
30
+ let manifestPath: string | null = null;
31
+ for (const p of manifestPaths) {
32
+ if (fs.existsSync(p)) {
33
+ manifestPath = p;
34
+ break;
35
+ }
36
+ }
37
+
38
+ if (!manifestPath) {
39
+ console.warn(
40
+ "[Hydration] Manifest file not found at any of:",
41
+ manifestPaths,
42
+ "- ensure build:hydration has been run",
43
+ );
44
+ return {};
45
+ }
46
+
47
+ try {
48
+ const content = fs.readFileSync(manifestPath, "utf-8");
49
+ cachedManifest = JSON.parse(content) as HydrationManifest;
50
+ return cachedManifest;
51
+ } catch (error) {
52
+ console.error("[Hydration] Failed to load hydration manifest:", error);
53
+ cachedManifest = {};
54
+ return cachedManifest;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Get the chunk filename for a specific component
60
+ */
61
+ export function getChunkFileName(componentName: string): string | null {
62
+ const manifest = getHydrationManifest();
63
+ return manifest[componentName] || null;
64
+ }
65
+
66
+ /**
67
+ * Get the public path for a component chunk
68
+ */
69
+ export function getChunkPath(componentName: string): string | null {
70
+ const fileName = getChunkFileName(componentName);
71
+ return fileName ? `/chunks/${fileName}` : null;
72
+ }
73
+
74
+ /**
75
+ * Invalidate cached manifest (useful for development mode with hot reload)
76
+ */
77
+ export function invalidateManifestCache(): void {
78
+ cachedManifest = null;
79
+ }