@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,313 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Build script to:
5
+ * 1. Analyze components for 'use client' directive
6
+ * 2. Generate separate hydration entry files
7
+ * 3. Bundle them with esbuild to dist/chunks with cache-busted names
8
+ * 4. Create a manifest file for server-side lookup
9
+ * 5. Create shared vendor bundle for React/ReactDOM to eliminate duplication
10
+ */
11
+
12
+ import { execSync } from "child_process";
13
+ import * as crypto from "crypto";
14
+ import * as fs from "fs";
15
+ import * as path from "path";
16
+
17
+ const PROJECT_ROOT = process.cwd();
18
+ const SRC_DIR = path.join(PROJECT_ROOT, "src");
19
+ const DIST_DIR = path.join(PROJECT_ROOT, "dist");
20
+ const HYDRATION_ENTRIES_DIR = path.join(SRC_DIR, ".hydration-entries");
21
+ const CHUNKS_DIR = path.join(DIST_DIR, "chunks");
22
+ // Write manifest directly to dist (no need for temp since this runs after nest build)
23
+ const MANIFEST_FILE = path.join(DIST_DIR, "hydration-manifest.json");
24
+ const VENDOR_BUNDLE = "vendor.js";
25
+
26
+ // Check if running in production mode (add cache-busting hashes only in production)
27
+ const IS_PRODUCTION = process.argv.includes("--prod");
28
+
29
+ interface ComponentFile {
30
+ filePath: string;
31
+ componentName: string;
32
+ isNamedExport: boolean; // Track if it's a named export
33
+ }
34
+
35
+ interface HydrationManifest {
36
+ [componentName: string]: string; // componentName -> chunkFileName
37
+ }
38
+
39
+ /**
40
+ * Find all files with 'use client' directive
41
+ */
42
+ function findClientComponents(): ComponentFile[] {
43
+ const components: ComponentFile[] = [];
44
+ const extensions = [".ts", ".tsx"];
45
+
46
+ function walkDir(dir: string) {
47
+ if (!fs.existsSync(dir)) return;
48
+
49
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
50
+
51
+ for (const entry of entries) {
52
+ const fullPath = path.join(dir, entry.name);
53
+
54
+ if (entry.isDirectory()) {
55
+ if (!entry.name.startsWith(".") && entry.name !== "node_modules") {
56
+ walkDir(fullPath);
57
+ }
58
+ } else if (extensions.includes(path.extname(entry.name))) {
59
+ try {
60
+ const content = fs.readFileSync(fullPath, "utf-8");
61
+
62
+ // Check for 'use client' directive at the start of the file
63
+ if (/^['"]use client['"]/.test(content.trim())) {
64
+ const fileName = path.basename(fullPath, path.extname(fullPath));
65
+ // Convert kebab-case to PascalCase
66
+ const componentName = fileName
67
+ .split("-")
68
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
69
+ .join("");
70
+
71
+ // Detect if it's a named export by checking for patterns like:
72
+ // export function ComponentName or export const ComponentName
73
+ const hasNamedExport = new RegExp(
74
+ `export\\s+(function|const|class)\\s+${componentName}\\b`,
75
+ ).test(content);
76
+
77
+ components.push({
78
+ filePath: fullPath,
79
+ componentName,
80
+ isNamedExport: hasNamedExport,
81
+ });
82
+ }
83
+ } catch (error) {
84
+ console.error(`Error reading file ${fullPath}:`, error);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ walkDir(SRC_DIR);
91
+ return components;
92
+ }
93
+
94
+ /**
95
+ * Generate hydration entry file for a client component
96
+ * Uses window.React from vendor bundle
97
+ */
98
+ function generateHydrationEntry(component: ComponentFile): string {
99
+ const relativePath = path.relative(HYDRATION_ENTRIES_DIR, component.filePath);
100
+ const importPath = `./${relativePath.replace(/\\\\/g, "/")}`;
101
+
102
+ // Generate appropriate import statement based on export type
103
+ const importStatement = component.isNamedExport
104
+ ? `import { ${component.componentName} } from '${importPath}';`
105
+ : `import ${component.componentName} from '${importPath}';`;
106
+
107
+ const content = `
108
+ // Use React from vendor bundle
109
+ const React = window.React;
110
+ const { hydrateRoot } = window.ReactDOM;
111
+
112
+ ${importStatement}
113
+
114
+ /**
115
+ * Auto-generated hydration entry for ${component.componentName}
116
+ */
117
+
118
+ // Find all hydration containers for this component
119
+ const containers = document.querySelectorAll('[id^="hydrate-${component.componentName}"]');
120
+
121
+ containers.forEach((container) => {
122
+ const containerElement = container as HTMLElement;
123
+ const propsElement = document.getElementById(\`\${container.id}-props\`);
124
+
125
+ let props = {};
126
+ if (propsElement) {
127
+ try {
128
+ props = JSON.parse(propsElement.textContent || '{}');
129
+ } catch (error) {
130
+ console.error('Error parsing props:', error);
131
+ }
132
+ }
133
+
134
+ try {
135
+ hydrateRoot(containerElement, React.createElement(${component.componentName}, props));
136
+ console.log('[Hydration] Hydrated ${component.componentName}');
137
+ } catch (error) {
138
+ console.error('[Hydration] Failed to hydrate ${component.componentName}:', error);
139
+ }
140
+ });
141
+ `.trim();
142
+
143
+ return content;
144
+ }
145
+
146
+ /**
147
+ * Generate cache-busted filename (production only)
148
+ * In dev mode, uses consistent filenames so the browser doesn't re-download
149
+ */
150
+ function generateChunkFilename(componentName: string): string {
151
+ if (IS_PRODUCTION) {
152
+ // Production: use cache-busting hash
153
+ const hash = crypto.randomBytes(8).toString("hex");
154
+ return `${componentName}.${hash}.js`;
155
+ } else {
156
+ // Development: use consistent filename
157
+ return `${componentName}.js`;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Main build process
163
+ */
164
+ function main(): void {
165
+ // Ensure hydration entries directory exists
166
+ if (!fs.existsSync(HYDRATION_ENTRIES_DIR)) {
167
+ fs.mkdirSync(HYDRATION_ENTRIES_DIR, { recursive: true });
168
+ }
169
+
170
+ console.log("šŸ” Detecting client components...");
171
+ const clientComponents = findClientComponents();
172
+
173
+ if (clientComponents.length === 0) {
174
+ console.log("āš ļø No client components found");
175
+ // Still ensure chunks directory exists and clear manifest
176
+ if (!fs.existsSync(CHUNKS_DIR)) {
177
+ fs.mkdirSync(CHUNKS_DIR, { recursive: true });
178
+ }
179
+ fs.writeFileSync(MANIFEST_FILE, JSON.stringify({}, null, 2), "utf-8");
180
+ return;
181
+ }
182
+
183
+ console.log(`āœ… Found ${clientComponents.length} client component(s):`);
184
+ clientComponents.forEach((c) => console.log(` - ${c.componentName}`));
185
+
186
+ // Create hydration entries directory
187
+ if (!fs.existsSync(HYDRATION_ENTRIES_DIR)) {
188
+ fs.mkdirSync(HYDRATION_ENTRIES_DIR, { recursive: true });
189
+ }
190
+
191
+ // Ensure chunks directory exists
192
+ if (!fs.existsSync(CHUNKS_DIR)) {
193
+ fs.mkdirSync(CHUNKS_DIR, { recursive: true });
194
+ }
195
+
196
+ console.log("\nšŸ“ Generating hydration entries...");
197
+
198
+ // Generate hydration entry files
199
+ const entryFiles: { path: string; componentName: string }[] = [];
200
+ for (const component of clientComponents) {
201
+ const entryContent = generateHydrationEntry(component);
202
+ const entryPath = path.join(
203
+ HYDRATION_ENTRIES_DIR,
204
+ `${component.componentName}.tsx`,
205
+ );
206
+
207
+ fs.writeFileSync(entryPath, entryContent, "utf-8");
208
+ entryFiles.push({
209
+ path: entryPath,
210
+ componentName: component.componentName,
211
+ });
212
+ console.log(` āœ“ ${component.componentName}.tsx`);
213
+ }
214
+
215
+ // Build shared vendor bundle first
216
+ console.log("\nšŸ“¦ Building shared vendor bundle...");
217
+ const vendorEntryPath = path.join(HYDRATION_ENTRIES_DIR, "_vendor.js");
218
+ const vendorContent = `
219
+ import React from 'react';
220
+ import ReactDOM from 'react-dom/client';
221
+
222
+ // Expose React and ReactDOM globally for component chunks
223
+ window.React = React;
224
+ window.ReactDOM = ReactDOM;
225
+ `.trim();
226
+
227
+ fs.writeFileSync(vendorEntryPath, vendorContent, "utf-8");
228
+
229
+ const vendorOutputPath = path.join(CHUNKS_DIR, VENDOR_BUNDLE);
230
+ try {
231
+ const vendorCommand = `npx esbuild "${vendorEntryPath}" --bundle --minify --target=es2020 --format=iife --outfile="${vendorOutputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV=\\"production\\"`;
232
+ execSync(vendorCommand, { stdio: "inherit" });
233
+ console.log(` āœ“ vendor.js (React + ReactDOM)`);
234
+ } catch (error) {
235
+ console.error(` āœ— Failed to bundle vendor:`, error);
236
+ process.exit(1);
237
+ }
238
+
239
+ // Bundle each entry file separately with cache-busted names
240
+ console.log("\nšŸ“¦ Bundling hydration scripts...");
241
+
242
+ // Create React shim files for aliasing
243
+ const SHIMS_DIR = path.join(DIST_DIR, ".shims");
244
+ if (!fs.existsSync(SHIMS_DIR)) {
245
+ fs.mkdirSync(SHIMS_DIR, { recursive: true });
246
+ }
247
+
248
+ // Create shim for react
249
+ const reactShimPath = path.join(SHIMS_DIR, "react.js");
250
+ fs.writeFileSync(reactShimPath, "module.exports = window.React;", "utf-8");
251
+
252
+ // Create shim for react-dom
253
+ const reactDomShimPath = path.join(SHIMS_DIR, "react-dom.js");
254
+ fs.writeFileSync(
255
+ reactDomShimPath,
256
+ "module.exports = window.ReactDOM;",
257
+ "utf-8",
258
+ );
259
+
260
+ // Create shim for react-dom/client
261
+ const reactDomClientShimPath = path.join(SHIMS_DIR, "react-dom-client.js");
262
+ fs.writeFileSync(
263
+ reactDomClientShimPath,
264
+ "module.exports = window.ReactDOM;",
265
+ "utf-8",
266
+ );
267
+
268
+ // Create shim for react/jsx-runtime
269
+ const jsxRuntimeShimPath = path.join(SHIMS_DIR, "jsx-runtime.js");
270
+ fs.writeFileSync(
271
+ jsxRuntimeShimPath,
272
+ `
273
+ const React = window.React;
274
+ module.exports = {
275
+ jsx: React.createElement,
276
+ jsxs: React.createElement,
277
+ Fragment: React.Fragment
278
+ };`,
279
+ "utf-8",
280
+ );
281
+
282
+ const manifest: HydrationManifest = {};
283
+
284
+ for (const entry of entryFiles) {
285
+ const chunkFilename = generateChunkFilename(entry.componentName);
286
+ const outputPath = path.join(CHUNKS_DIR, chunkFilename);
287
+
288
+ try {
289
+ // Use aliases to redirect React imports to window.React from vendor bundle
290
+ const command = `npx esbuild "${entry.path}" --bundle --minify --target=es2020 --format=iife --keep-names --outfile="${outputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV=\\"production\\" --alias:react=${reactShimPath} --alias:react-dom=${reactDomShimPath} --alias:react-dom/client=${reactDomClientShimPath} --alias:react/jsx-runtime=${jsxRuntimeShimPath}`;
291
+ execSync(command, { stdio: "inherit" });
292
+ manifest[entry.componentName] = chunkFilename;
293
+ console.log(` āœ“ ${entry.componentName} -> ${chunkFilename}`);
294
+ } catch (error) {
295
+ console.error(` āœ— Failed to bundle ${entry.componentName}:`, error);
296
+ process.exit(1);
297
+ }
298
+ }
299
+
300
+ // Write manifest file for server-side lookup
301
+ console.log("\nšŸ“‹ Writing hydration manifest...");
302
+ fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2), "utf-8");
303
+ console.log(` āœ“ Manifest written to ${MANIFEST_FILE}`);
304
+
305
+ // Clean up temporary entries directory
306
+ if (fs.existsSync(HYDRATION_ENTRIES_DIR)) {
307
+ fs.rmSync(HYDRATION_ENTRIES_DIR, { recursive: true });
308
+ }
309
+
310
+ console.log("\n✨ Hydration build complete!");
311
+ }
312
+
313
+ main();
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Build script for common CSS file
4
+ * Generates a single styles.css with all Tailwind styles for the entire app
5
+ */
6
+
7
+ import { execSync } from "child_process";
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+
11
+ const projectRoot = process.cwd();
12
+ const distDir = path.join(projectRoot, "dist");
13
+ const stylesDir = path.join(distDir, "styles");
14
+ const srcAssetsDir = path.join(projectRoot, "src/assets");
15
+ const outputCssPath = path.join(stylesDir, "styles.css");
16
+
17
+ async function main(): Promise<void> {
18
+ console.log("šŸŽØ Building styles...");
19
+
20
+ try {
21
+ // Ensure styles directory exists
22
+ if (!fs.existsSync(stylesDir)) {
23
+ fs.mkdirSync(stylesDir, { recursive: true });
24
+ }
25
+
26
+ // Compile Tailwind CSS
27
+ console.log(" Compiling Tailwind CSS...");
28
+ execSync(
29
+ `NODE_ENV=production postcss ${path.join(srcAssetsDir, "styles.css")} -o ${outputCssPath}`,
30
+ {
31
+ stdio: "inherit",
32
+ },
33
+ );
34
+
35
+ console.log(` āœ“ Generated styles.css`);
36
+ console.log("✨ Styles build complete!");
37
+ } catch (error: any) {
38
+ console.error("āŒ CSS generation failed:", error.message);
39
+ process.exit(1);
40
+ }
41
+ }
42
+
43
+ main();
@@ -0,0 +1,34 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+
4
+ const src = "public";
5
+ const dest = path.join("dist", "public");
6
+
7
+ if (!fs.existsSync(src)) {
8
+ console.log("No public folder to copy");
9
+ process.exit(0);
10
+ }
11
+
12
+ if (fs.existsSync(dest)) {
13
+ fs.rmSync(dest, { recursive: true });
14
+ }
15
+
16
+ const copy = (source: string, target: string): void => {
17
+ fs.mkdirSync(target, { recursive: true });
18
+
19
+ for (const entry of fs.readdirSync(source)) {
20
+ const srcPath = path.join(source, entry);
21
+ const destPath = path.join(target, entry);
22
+ const stat = fs.statSync(srcPath);
23
+
24
+ if (stat.isDirectory()) {
25
+ copy(srcPath, destPath);
26
+ } else {
27
+ fs.copyFileSync(srcPath, destPath);
28
+ }
29
+ }
30
+ };
31
+
32
+ copy(src, dest);
33
+
34
+ console.log("Assets copied successfully");
package/scripts/dev.sh ADDED
@@ -0,0 +1,3 @@
1
+ #!/bin/bash
2
+ # Development script that rebuilds hydration on file changes
3
+ pnpm ts-node scripts/dev.ts
package/scripts/dev.ts ADDED
@@ -0,0 +1,257 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import * as fs from "fs";
5
+ import * as http from "http";
6
+
7
+ let nestProcess: any = null;
8
+ let isRebuilding = false;
9
+
10
+ /**
11
+ * Trigger browser reload by sending notification to LiveReloadController
12
+ */
13
+ function triggerBrowserReload(): void {
14
+ const options = {
15
+ hostname: "127.0.0.1",
16
+ port: 3000,
17
+ path: "/__harpy/live-reload/trigger",
18
+ method: "POST",
19
+ };
20
+
21
+ const req = http.request(options, () => {
22
+ // Silently succeed
23
+ });
24
+
25
+ req.on("error", () => {
26
+ // Silently fail - server might not be ready yet
27
+ });
28
+
29
+ req.end();
30
+ }
31
+
32
+ async function runCommand(cmd: string, args: string[] = []): Promise<void> {
33
+ return new Promise((resolve, reject) => {
34
+ const proc = spawn(cmd, args, { stdio: "inherit", shell: true });
35
+ proc.on("close", (code) => {
36
+ if (code !== 0) {
37
+ reject(new Error(`Command failed with code ${code}`));
38
+ } else {
39
+ resolve();
40
+ }
41
+ });
42
+ });
43
+ }
44
+
45
+ async function buildHydration(): Promise<void> {
46
+ console.log("šŸ”§ Building hydration...");
47
+ try {
48
+ await runCommand("pnpm", ["build:hydration"]);
49
+ console.log("āœ… Hydration built");
50
+ } catch (error) {
51
+ console.error("āŒ Hydration build failed:", error);
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ async function autoWrap(): Promise<void> {
57
+ console.log("šŸ”„ Auto-wrapping client components...");
58
+ try {
59
+ await runCommand("pnpm", ["auto-wrap"]);
60
+ console.log("āœ… Auto-wrap complete");
61
+ } catch (error) {
62
+ console.error("āŒ Auto-wrap failed:", error);
63
+ throw error;
64
+ }
65
+ }
66
+
67
+ async function buildStyles(): Promise<void> {
68
+ console.log("šŸŽØ Building styles...");
69
+ try {
70
+ await runCommand("pnpm", ["build:styles"]);
71
+ console.log("āœ… Styles built");
72
+ } catch (error) {
73
+ console.error("āŒ Styles build failed:", error);
74
+ throw error;
75
+ }
76
+ }
77
+
78
+ async function startNestServer(): Promise<void> {
79
+ return new Promise((resolve) => {
80
+ console.log("šŸš€ Starting NestJS server from compiled dist...");
81
+ // Run compiled dist/main.js instead of using ts-node
82
+ // This ensures auto-wrapped components are used
83
+ nestProcess = spawn("node", ["--watch", "dist/main.js"], {
84
+ stdio: "pipe",
85
+ shell: false,
86
+ cwd: process.cwd(),
87
+ });
88
+
89
+ let resolved = false;
90
+ let isFirstStart = true;
91
+
92
+ // Capture output to detect when server is ready
93
+ nestProcess.stdout?.on("data", (data: Buffer) => {
94
+ const output = data.toString();
95
+ process.stdout.write(output);
96
+
97
+ // Detect when NestJS application successfully started
98
+ if (output.includes("Nest application successfully started")) {
99
+ if (!resolved && isFirstStart) {
100
+ // First start - resolve the promise
101
+ resolved = true;
102
+ isFirstStart = false;
103
+ resolve();
104
+ } else if (!isFirstStart && !isRebuilding) {
105
+ // Subsequent restarts - rebuild assets
106
+ setTimeout(async () => {
107
+ if (isRebuilding) return;
108
+ isRebuilding = true;
109
+ console.log("\nšŸ”„ NestJS rebuild detected, rebuilding assets...");
110
+ try {
111
+ await buildHydration();
112
+ await autoWrap();
113
+ await buildStyles();
114
+ console.log("āœ… Assets rebuilt\n");
115
+ triggerBrowserReload();
116
+ } catch (error) {
117
+ console.error("āŒ Asset rebuild failed:", error);
118
+ } finally {
119
+ isRebuilding = false;
120
+ }
121
+ }, 100);
122
+ }
123
+ }
124
+ });
125
+
126
+ nestProcess.stderr?.on("data", (data: Buffer) => {
127
+ process.stderr.write(data);
128
+ });
129
+
130
+ // Fallback timeout in case the detection fails
131
+ setTimeout(() => {
132
+ if (!resolved) {
133
+ resolved = true;
134
+ isFirstStart = false;
135
+ resolve();
136
+ }
137
+ }, 8000);
138
+ });
139
+ }
140
+
141
+ function watchSourceChanges(): void {
142
+ console.log("šŸ‘€ Watching source files for changes...");
143
+
144
+ let debounceTimer: NodeJS.Timeout | null = null;
145
+ const watchedFiles = new Set<string>();
146
+
147
+ // Watch src for CSS changes only (TS/TSX changes are handled by NestJS watch + stdout detection)
148
+ fs.watch("src", { recursive: true }, async (eventType, filename) => {
149
+ if (!filename || isRebuilding) return;
150
+
151
+ // Ignore .hydration-entries changes
152
+ if (filename.includes(".hydration-entries")) {
153
+ return;
154
+ }
155
+
156
+ // Only watch CSS files (TS/TSX changes trigger nest rebuild which we catch above)
157
+ if (!filename.endsWith(".css")) return;
158
+
159
+ // Skip if we just rebuilt for this file (avoid duplicate rebuilds)
160
+ if (watchedFiles.has(filename)) {
161
+ return;
162
+ }
163
+
164
+ watchedFiles.add(filename);
165
+
166
+ // Debounce: wait 1000ms after last change before rebuilding
167
+ if (debounceTimer) clearTimeout(debounceTimer);
168
+
169
+ debounceTimer = setTimeout(async () => {
170
+ isRebuilding = true;
171
+ console.log(`\nšŸ“ CSS file changed: ${filename}`);
172
+
173
+ try {
174
+ await buildStyles();
175
+ console.log("āœ… Styles rebuilt\n");
176
+ triggerBrowserReload();
177
+ } catch (error) {
178
+ console.error("Build error:", error);
179
+ } finally {
180
+ watchedFiles.delete(filename);
181
+ isRebuilding = false;
182
+ }
183
+ }, 1000);
184
+ });
185
+ }
186
+
187
+ let tscProcess: any = null;
188
+
189
+ async function startTypeScriptWatch(): Promise<void> {
190
+ return new Promise((resolve) => {
191
+ console.log("āš™ļø Starting TypeScript compiler in watch mode...");
192
+ tscProcess = spawn("pnpm", ["nest", "build", "--watch"], {
193
+ stdio: "pipe",
194
+ shell: true,
195
+ });
196
+
197
+ let resolved = false;
198
+
199
+ tscProcess.stdout?.on("data", (data: Buffer) => {
200
+ const output = data.toString();
201
+ process.stdout.write(output);
202
+
203
+ // Resolve once first compilation is done
204
+ if (output.includes("Found 0 errors. Watching") && !resolved) {
205
+ resolved = true;
206
+ resolve();
207
+ }
208
+ });
209
+
210
+ tscProcess.stderr?.on("data", (data: Buffer) => {
211
+ process.stderr.write(data);
212
+ });
213
+ });
214
+ }
215
+
216
+ async function main(): Promise<void> {
217
+ try {
218
+ console.log("šŸ“¦ Initializing development environment...\n");
219
+
220
+ // First: Start TypeScript compiler in watch mode
221
+ await startTypeScriptWatch();
222
+
223
+ // Build initial assets after first compilation
224
+ console.log("\nšŸ”§ Building hydration assets...");
225
+ await buildHydration();
226
+ await autoWrap();
227
+ await buildStyles();
228
+
229
+ // Now start the node server with compiled dist files
230
+ await startNestServer();
231
+
232
+ console.log("\nāœ… Development server ready!\n");
233
+
234
+ // Watch for source changes
235
+ watchSourceChanges();
236
+
237
+ // Handle graceful shutdown
238
+ process.on("SIGINT", async () => {
239
+ console.log("\n\nšŸ›‘ Stopping development server...");
240
+ if (tscProcess) tscProcess.kill();
241
+ if (nestProcess) nestProcess.kill();
242
+ process.exit(0);
243
+ });
244
+
245
+ process.on("SIGTERM", async () => {
246
+ console.log("\n\nšŸ›‘ Stopping development server...");
247
+ if (tscProcess) tscProcess.kill();
248
+ if (nestProcess) nestProcess.kill();
249
+ process.exit(0);
250
+ });
251
+ } catch (error) {
252
+ console.error("Fatal error:", error);
253
+ process.exit(1);
254
+ }
255
+ }
256
+
257
+ main();