@harpy-js/core 0.5.2 → 0.5.4

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/dist/cli.js CHANGED
@@ -8,11 +8,13 @@ const scripts = {
8
8
  "build-hydration": path.join(__dirname, "../scripts/build-hydration.ts"),
9
9
  "auto-wrap": path.join(__dirname, "../scripts/auto-wrap-exports.ts"),
10
10
  "build-styles": path.join(__dirname, "../scripts/build-page-styles.ts"),
11
+ build: path.join(__dirname, "../scripts/build.ts"),
12
+ start: path.join(__dirname, "../scripts/start.ts"),
11
13
  dev: path.join(__dirname, "../scripts/dev.ts"),
12
14
  };
13
15
  if (!command || !scripts[command]) {
14
16
  console.error("Usage: harpy <command>");
15
- console.error("Commands: build-hydration, auto-wrap, build-styles, dev");
17
+ console.error("Commands: build, start, dev, build-hydration, auto-wrap, build-styles");
16
18
  process.exit(1);
17
19
  }
18
20
  const scriptPath = scripts[command];
@@ -2,6 +2,7 @@ import type { NestFastifyApplication } from "@nestjs/platform-fastify";
2
2
  export interface HarpyAppOptions {
3
3
  layout?: any;
4
4
  distDir?: string;
5
+ publicDir?: string;
5
6
  }
6
7
  export declare function configureHarpyApp(app: NestFastifyApplication, opts?: HarpyAppOptions): Promise<void>;
7
8
  export declare function setupHarpyApp(app: NestFastifyApplication, opts?: HarpyAppOptions): Promise<void>;
@@ -52,7 +52,7 @@ catch (e) {
52
52
  }
53
53
  const jsx_engine_1 = require("./jsx.engine");
54
54
  async function configureHarpyApp(app, opts = {}) {
55
- const { layout, distDir = "dist" } = opts;
55
+ const { layout, distDir = "dist", publicDir } = opts;
56
56
  if (layout) {
57
57
  (0, jsx_engine_1.withJsxEngine)(app, layout);
58
58
  }
@@ -69,6 +69,13 @@ async function configureHarpyApp(app, opts = {}) {
69
69
  prefix: "/",
70
70
  decorateReply: false,
71
71
  });
72
+ if (publicDir) {
73
+ await fastify.register(fastifyStatic, {
74
+ root: path.join(process.cwd(), publicDir),
75
+ prefix: "/",
76
+ decorateReply: false,
77
+ });
78
+ }
72
79
  }
73
80
  else {
74
81
  console.warn("[harpy-core] optional dependency `@fastify/static` is not installed; static `dist` handler not registered.");
package/dist/index.d.ts CHANGED
@@ -19,3 +19,4 @@ export { configureHarpyApp, HarpyAppOptions } from "./core/app-setup";
19
19
  export { setupHarpyApp } from "./core/app-setup";
20
20
  export { default as Link } from "./client/Link";
21
21
  export { buildHrefIndex, getActiveItemIdFromIndex, getActiveItemIdFromManifest, } from "./client/getActiveItemId";
22
+ export { useI18n } from "./client/use-i18n";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
6
+ exports.useI18n = exports.getActiveItemIdFromManifest = exports.getActiveItemIdFromIndex = exports.buildHrefIndex = exports.Link = exports.setupHarpyApp = exports.configureHarpyApp = exports.AutoRegisterModule = exports.NavigationService = exports.RouterModule = exports.DefaultSeoService = exports.BaseSeoService = exports.SeoModule = exports.WithLayout = exports.JsxRender = exports.StaticAssetsController = exports.LiveReloadController = exports.withJsxEngine = exports.getHydrationManifest = exports.getChunkPath = exports.initializeHydrationContext = exports.hydrationContext = exports.autoWrapClientComponent = void 0;
7
7
  var client_component_wrapper_1 = require("./core/client-component-wrapper");
8
8
  Object.defineProperty(exports, "autoWrapClientComponent", { enumerable: true, get: function () { return client_component_wrapper_1.autoWrapClientComponent; } });
9
9
  var hydration_1 = require("./core/hydration");
@@ -42,3 +42,5 @@ var getActiveItemId_1 = require("./client/getActiveItemId");
42
42
  Object.defineProperty(exports, "buildHrefIndex", { enumerable: true, get: function () { return getActiveItemId_1.buildHrefIndex; } });
43
43
  Object.defineProperty(exports, "getActiveItemIdFromIndex", { enumerable: true, get: function () { return getActiveItemId_1.getActiveItemIdFromIndex; } });
44
44
  Object.defineProperty(exports, "getActiveItemIdFromManifest", { enumerable: true, get: function () { return getActiveItemId_1.getActiveItemIdFromManifest; } });
45
+ var use_i18n_1 = require("./client/use-i18n");
46
+ Object.defineProperty(exports, "useI18n", { enumerable: true, get: function () { return use_i18n_1.useI18n; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@harpy-js/core",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Harpy - A powerful NestJS + React/JSX SSR framework with automatic hydration and i18n support",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,6 +11,9 @@
11
11
 
12
12
  import * as fs from "fs";
13
13
  import * as path from "path";
14
+ import { Logger } from "./logger";
15
+
16
+ const logger = new Logger("AutoWrapper");
14
17
 
15
18
  const PROJECT_ROOT = process.cwd();
16
19
  const SRC_DIR = path.join(PROJECT_ROOT, "src");
@@ -213,16 +216,16 @@ function transformCompiledFile(
213
216
  * Main function
214
217
  */
215
218
  function main(): void {
216
- console.log("🔄 Auto-wrapping client component exports...\n");
219
+ logger.log("Auto-wrapping client component exports...");
217
220
 
218
221
  const clientComponents = findClientComponentsInSource();
219
222
 
220
223
  if (clientComponents.size === 0) {
221
- console.log("⚠️ No client components found\n");
224
+ logger.warn("No client components found");
222
225
  return;
223
226
  }
224
227
 
225
- console.log(`Found ${clientComponents.size} client component(s):\n`);
228
+ logger.log(`Found ${clientComponents.size} client component(s)`);
226
229
 
227
230
  let wrapped = 0;
228
231
  for (const [compiledPath, componentName] of clientComponents) {
@@ -231,9 +234,7 @@ function main(): void {
231
234
  }
232
235
  }
233
236
 
234
- console.log(
235
- `\n✨ Auto-wrap complete: ${wrapped}/${clientComponents.size} components wrapped\n`,
236
- );
237
+ logger.log(`Auto-wrap complete: ${wrapped}/${clientComponents.size} components wrapped`);
237
238
  }
238
239
 
239
240
  main();
@@ -13,6 +13,9 @@ import { execSync } from "child_process";
13
13
  import * as crypto from "crypto";
14
14
  import * as fs from "fs";
15
15
  import * as path from "path";
16
+ import { Logger } from "./logger";
17
+
18
+ const logger = new Logger("HydrationBuilder");
16
19
 
17
20
  const PROJECT_ROOT = process.cwd();
18
21
  const SRC_DIR = path.join(PROJECT_ROOT, "src");
@@ -181,11 +184,11 @@ function main(): void {
181
184
  fs.mkdirSync(HYDRATION_ENTRIES_DIR, { recursive: true });
182
185
  }
183
186
 
184
- console.log("🔍 Detecting client components...");
187
+ logger.log("Detecting client components...");
185
188
  const clientComponents = findClientComponents();
186
189
 
187
190
  if (clientComponents.length === 0) {
188
- console.log("⚠️ No client components found");
191
+ logger.warn("No client components found");
189
192
  // Still ensure chunks directory exists and clear manifest
190
193
  if (!fs.existsSync(CHUNKS_DIR)) {
191
194
  fs.mkdirSync(CHUNKS_DIR, { recursive: true });
@@ -207,7 +210,7 @@ function main(): void {
207
210
  fs.mkdirSync(CHUNKS_DIR, { recursive: true });
208
211
  }
209
212
 
210
- console.log("\n📝 Generating hydration entries...");
213
+ logger.log("Generating hydration entries...");
211
214
 
212
215
  // Generate hydration entry files
213
216
  const entryFiles: { path: string; componentName: string }[] = [];
@@ -223,11 +226,10 @@ function main(): void {
223
226
  path: entryPath,
224
227
  componentName: component.componentName,
225
228
  });
226
- console.log(` ✓ ${component.componentName}.tsx`);
227
229
  }
228
230
 
229
231
  // Build shared vendor bundle first
230
- console.log("\n📦 Building shared vendor bundle...");
232
+ logger.log("Building shared vendor bundle...");
231
233
  const vendorEntryPath = path.join(HYDRATION_ENTRIES_DIR, "_vendor.js");
232
234
  const vendorContent = `
233
235
  import React from 'react';
@@ -242,16 +244,15 @@ window.ReactDOM = ReactDOM;
242
244
 
243
245
  const vendorOutputPath = path.join(CHUNKS_DIR, VENDOR_BUNDLE);
244
246
  try {
245
- const vendorCommand = `npx esbuild "${vendorEntryPath}" --bundle --minify --target=es2020 --format=iife --outfile="${vendorOutputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV=\\"production\\"`;
247
+ const vendorCommand = `npx esbuild "${vendorEntryPath}" --bundle --minify --target=es2020 --format=iife --outfile="${vendorOutputPath}" --platform=browser --tree-shaking=true --define:process.env.NODE_ENV='"production"'`;
246
248
  execSync(vendorCommand, { stdio: "inherit" });
247
- console.log(` ✓ vendor.js (React + ReactDOM)`);
248
249
  } catch (error) {
249
- console.error(`Failed to bundle vendor:`, error);
250
+ logger.error(`Failed to bundle vendor: ${error}`);
250
251
  process.exit(1);
251
252
  }
252
253
 
253
254
  // Bundle each entry file separately with cache-busted names
254
- console.log("\n📦 Bundling hydration scripts...");
255
+ logger.log("Bundling hydration scripts...");
255
256
 
256
257
  // Create React shim files for aliasing
257
258
  const SHIMS_DIR = path.join(DIST_DIR, ".shims");
@@ -301,27 +302,25 @@ module.exports = {
301
302
 
302
303
  try {
303
304
  // Use aliases to redirect React imports to window.React from vendor bundle
304
- 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}`;
305
+ 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}`;
305
306
  execSync(command, { stdio: "inherit" });
306
307
  manifest[entry.componentName] = chunkFilename;
307
- console.log(` ✓ ${entry.componentName} -> ${chunkFilename}`);
308
308
  } catch (error) {
309
- console.error(`Failed to bundle ${entry.componentName}:`, error);
309
+ logger.error(`Failed to bundle ${entry.componentName}: ${error}`);
310
310
  process.exit(1);
311
311
  }
312
312
  }
313
313
 
314
314
  // Write manifest file for server-side lookup
315
- console.log("\n📋 Writing hydration manifest...");
315
+ logger.log("Writing hydration manifest...");
316
316
  fs.writeFileSync(MANIFEST_FILE, JSON.stringify(manifest, null, 2), "utf-8");
317
- console.log(` ✓ Manifest written to ${MANIFEST_FILE}`);
318
317
 
319
318
  // Clean up temporary entries directory
320
319
  if (fs.existsSync(HYDRATION_ENTRIES_DIR)) {
321
320
  fs.rmSync(HYDRATION_ENTRIES_DIR, { recursive: true });
322
321
  }
323
322
 
324
- console.log("\n✨ Hydration build complete!");
323
+ logger.log("Hydration build complete!");
325
324
  }
326
325
 
327
326
  main();
@@ -7,7 +7,9 @@
7
7
  import { execSync } from "child_process";
8
8
  import * as fs from "fs";
9
9
  import * as path from "path";
10
+ import { Logger } from "./logger";
10
11
 
12
+ const logger = new Logger("StylesBuilder");
11
13
  const projectRoot = process.cwd();
12
14
  const distDir = path.join(projectRoot, "dist");
13
15
  const stylesDir = path.join(distDir, "styles");
@@ -15,7 +17,7 @@ const srcAssetsDir = path.join(projectRoot, "src/assets");
15
17
  const outputCssPath = path.join(stylesDir, "styles.css");
16
18
 
17
19
  async function main(): Promise<void> {
18
- console.log("🎨 Building styles...");
20
+ logger.log("Building styles...");
19
21
 
20
22
  try {
21
23
  // Ensure styles directory exists
@@ -24,7 +26,7 @@ async function main(): Promise<void> {
24
26
  }
25
27
 
26
28
  // Compile Tailwind CSS
27
- console.log(" Compiling Tailwind CSS...");
29
+ logger.log("Compiling Tailwind CSS...");
28
30
  execSync(
29
31
  `NODE_ENV=production postcss ${path.join(srcAssetsDir, "styles.css")} -o ${outputCssPath}`,
30
32
  {
@@ -32,10 +34,9 @@ async function main(): Promise<void> {
32
34
  },
33
35
  );
34
36
 
35
- console.log(` ✓ Generated styles.css`);
36
- console.log("✨ Styles build complete!");
37
+ logger.log("Styles build complete!");
37
38
  } catch (error: any) {
38
- console.error("❌ CSS generation failed:", error.message);
39
+ logger.error(`CSS generation failed: ${error.message}`);
39
40
  process.exit(1);
40
41
  }
41
42
  }
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { Logger } from "./logger";
5
+
6
+ const logger = new Logger("Builder");
7
+
8
+ async function runCommand(cmd: string, args: string[] = []): Promise<void> {
9
+ return new Promise((resolve, reject) => {
10
+ const proc = spawn(cmd, args, { stdio: "inherit", shell: true });
11
+ proc.on("close", (code) => {
12
+ if (code !== 0) {
13
+ reject(new Error(`Command failed with code ${code}`));
14
+ } else {
15
+ resolve();
16
+ }
17
+ });
18
+ });
19
+ }
20
+
21
+ async function buildNestApp(): Promise<void> {
22
+ logger.log("Building NestJS application...");
23
+ try {
24
+ await runCommand("nest", ["build"]);
25
+ } catch (error) {
26
+ logger.error(`NestJS build failed: ${error}`);
27
+ throw error;
28
+ }
29
+ }
30
+
31
+ async function buildHydration(): Promise<void> {
32
+ logger.log("Building hydration components...");
33
+ try {
34
+ const tsxPath = require("child_process")
35
+ .execSync("which tsx", { encoding: "utf-8" })
36
+ .trim();
37
+ await runCommand(tsxPath, [
38
+ require("path").join(__dirname, "build-hydration.ts"),
39
+ ]);
40
+ } catch (error) {
41
+ logger.error(`Hydration build failed: ${error}`);
42
+ throw error;
43
+ }
44
+ }
45
+
46
+ async function autoWrap(): Promise<void> {
47
+ logger.log("Auto-wrapping client components...");
48
+ try {
49
+ const tsxPath = require("child_process")
50
+ .execSync("which tsx", { encoding: "utf-8" })
51
+ .trim();
52
+ await runCommand(tsxPath, [
53
+ require("path").join(__dirname, "auto-wrap-exports.ts"),
54
+ ]);
55
+ } catch (error) {
56
+ logger.error(`Auto-wrap failed: ${error}`);
57
+ throw error;
58
+ }
59
+ }
60
+
61
+ async function buildStyles(): Promise<void> {
62
+ logger.log("Building styles...");
63
+ try {
64
+ const tsxPath = require("child_process")
65
+ .execSync("which tsx", { encoding: "utf-8" })
66
+ .trim();
67
+ await runCommand(tsxPath, [
68
+ require("path").join(__dirname, "build-page-styles.ts"),
69
+ ]);
70
+ } catch (error) {
71
+ logger.error(`Styles build failed: ${error}`);
72
+ throw error;
73
+ }
74
+ }
75
+
76
+ async function main(): Promise<void> {
77
+ try {
78
+ logger.log("Starting production build...");
79
+
80
+ await buildNestApp();
81
+ await buildHydration();
82
+ await autoWrap();
83
+ await buildStyles();
84
+
85
+ logger.log("Production build complete!");
86
+ process.exit(0);
87
+ } catch (error) {
88
+ logger.error(`Build failed: ${error}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+
93
+ main();
package/scripts/dev.ts CHANGED
@@ -3,6 +3,9 @@
3
3
  import { spawn } from "child_process";
4
4
  import * as fs from "fs";
5
5
  import * as http from "http";
6
+ import { Logger } from "./logger";
7
+
8
+ const logger = new Logger("DevServer");
6
9
 
7
10
  let nestProcess: any = null;
8
11
  let isRebuilding = false;
@@ -43,43 +46,52 @@ async function runCommand(cmd: string, args: string[] = []): Promise<void> {
43
46
  }
44
47
 
45
48
  async function buildHydration(): Promise<void> {
46
- console.log("🔧 Building hydration...");
49
+ logger.log("Building hydration components...");
47
50
  try {
48
- await runCommand("pnpm", ["build:hydration"]);
49
- console.log(" Hydration built");
51
+ const tsxPath = require("child_process")
52
+ .execSync("which tsx", { encoding: "utf-8" })
53
+ .trim();
54
+ await runCommand(tsxPath, [
55
+ require("path").join(__dirname, "build-hydration.ts"),
56
+ ]);
50
57
  } catch (error) {
51
- console.error("❌ Hydration build failed:", error);
58
+ logger.error(`Hydration build failed: ${error}`);
52
59
  throw error;
53
60
  }
54
61
  }
55
62
 
56
63
  async function autoWrap(): Promise<void> {
57
- console.log("🔄 Auto-wrapping client components...");
64
+ logger.log("Auto-wrapping client components...");
58
65
  try {
59
- await runCommand("pnpm", ["auto-wrap"]);
60
- console.log(" Auto-wrap complete");
66
+ const tsxPath = require("child_process")
67
+ .execSync("which tsx", { encoding: "utf-8" })
68
+ .trim();
69
+ await runCommand(tsxPath, [
70
+ require("path").join(__dirname, "auto-wrap-exports.ts"),
71
+ ]);
61
72
  } catch (error) {
62
- console.error("❌ Auto-wrap failed:", error);
63
- throw error;
73
+ logger.error(`Auto-wrap failed: ${error}`);
64
74
  }
65
75
  }
66
76
 
67
77
  async function buildStyles(): Promise<void> {
68
- console.log("🎨 Building styles...");
78
+ logger.log("Building styles...");
69
79
  try {
70
- await runCommand("pnpm", ["build:styles"]);
71
- console.log(" Styles built");
80
+ const tsxPath = require("child_process")
81
+ .execSync("which tsx", { encoding: "utf-8" })
82
+ .trim();
83
+ await runCommand(tsxPath, [
84
+ require("path").join(__dirname, "build-page-styles.ts"),
85
+ ]);
72
86
  } catch (error) {
73
- console.error("❌ Styles build failed:", error);
74
- throw error;
87
+ logger.error(`Styles build failed: ${error}`);
75
88
  }
76
89
  }
77
90
 
78
91
  async function startNestServer(): Promise<void> {
79
92
  return new Promise((resolve) => {
80
- console.log("🚀 Starting NestJS server from compiled dist...");
93
+ logger.log("Starting NestJS application...");
81
94
  // Run compiled dist/main.js instead of using ts-node
82
- // This ensures auto-wrapped components are used
83
95
  nestProcess = spawn("node", ["--watch", "dist/main.js"], {
84
96
  stdio: "pipe",
85
97
  shell: false,
@@ -106,15 +118,14 @@ async function startNestServer(): Promise<void> {
106
118
  setTimeout(async () => {
107
119
  if (isRebuilding) return;
108
120
  isRebuilding = true;
109
- console.log("\n🔄 NestJS rebuild detected, rebuilding assets...");
121
+ logger.log("Rebuilding assets after code change...");
110
122
  try {
111
123
  await buildHydration();
112
124
  await autoWrap();
113
125
  await buildStyles();
114
- console.log("✅ Assets rebuilt\n");
115
126
  triggerBrowserReload();
116
127
  } catch (error) {
117
- console.error("❌ Asset rebuild failed:", error);
128
+ logger.error(`Asset rebuild failed: ${error}`);
118
129
  } finally {
119
130
  isRebuilding = false;
120
131
  }
@@ -139,10 +150,9 @@ async function startNestServer(): Promise<void> {
139
150
  }
140
151
 
141
152
  function watchSourceChanges(): void {
142
- console.log("👀 Watching source files for changes...");
153
+ logger.log("Watching source files for changes...");
143
154
 
144
155
  let debounceTimer: NodeJS.Timeout | null = null;
145
- const watchedFiles = new Set<string>();
146
156
 
147
157
  // Watch src for CSS changes only (TS/TSX changes are handled by NestJS watch + stdout detection)
148
158
  fs.watch("src", { recursive: true }, async (eventType, filename) => {
@@ -168,14 +178,13 @@ function watchSourceChanges(): void {
168
178
 
169
179
  debounceTimer = setTimeout(async () => {
170
180
  isRebuilding = true;
171
- console.log(`\n📝 CSS file changed: ${filename}`);
181
+ logger.log(`CSS file changed: ${filename}`);
172
182
 
173
183
  try {
174
184
  await buildStyles();
175
- console.log("✅ Styles rebuilt\n");
176
185
  triggerBrowserReload();
177
186
  } catch (error) {
178
- console.error("Build error:", error);
187
+ logger.error(`Style rebuild failed: ${error}`);
179
188
  } finally {
180
189
  watchedFiles.delete(filename);
181
190
  isRebuilding = false;
@@ -188,7 +197,7 @@ let tscProcess: any = null;
188
197
 
189
198
  async function startTypeScriptWatch(): Promise<void> {
190
199
  return new Promise((resolve) => {
191
- console.log("⚙️ Starting TypeScript compiler in watch mode...");
200
+ logger.log("Starting TypeScript compiler in watch mode...");
192
201
  tscProcess = spawn("pnpm", ["nest", "build", "--watch"], {
193
202
  stdio: "pipe",
194
203
  shell: true,
@@ -215,13 +224,13 @@ async function startTypeScriptWatch(): Promise<void> {
215
224
 
216
225
  async function main(): Promise<void> {
217
226
  try {
218
- console.log("📦 Initializing development environment...\n");
227
+ logger.log("Initializing development environment...");
219
228
 
220
229
  // First: Start TypeScript compiler in watch mode
221
230
  await startTypeScriptWatch();
222
231
 
223
232
  // Build initial assets after first compilation
224
- console.log("\n🔧 Building hydration assets...");
233
+ logger.log("Building initial assets...");
225
234
  await buildHydration();
226
235
  await autoWrap();
227
236
  await buildStyles();
@@ -229,27 +238,27 @@ async function main(): Promise<void> {
229
238
  // Now start the node server with compiled dist files
230
239
  await startNestServer();
231
240
 
232
- console.log("\n✅ Development server ready!\n");
241
+ logger.log("Development server ready!");
233
242
 
234
243
  // Watch for source changes
235
244
  watchSourceChanges();
236
245
 
237
246
  // Handle graceful shutdown
238
247
  process.on("SIGINT", async () => {
239
- console.log("\n\n🛑 Stopping development server...");
248
+ logger.log("Stopping development server...");
240
249
  if (tscProcess) tscProcess.kill();
241
250
  if (nestProcess) nestProcess.kill();
242
251
  process.exit(0);
243
252
  });
244
253
 
245
254
  process.on("SIGTERM", async () => {
246
- console.log("\n\n🛑 Stopping development server...");
255
+ logger.log("Stopping development server...");
247
256
  if (tscProcess) tscProcess.kill();
248
257
  if (nestProcess) nestProcess.kill();
249
258
  process.exit(0);
250
259
  });
251
260
  } catch (error) {
252
- console.error("Fatal error:", error);
261
+ logger.error(`Fatal error: ${error}`);
253
262
  process.exit(1);
254
263
  }
255
264
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Logger utility to format logs matching NestJS style
3
+ * Format: [Harpy] PID - DATE, TIME LEVEL [Context] Message +Xms
4
+ */
5
+
6
+ const pid = process.pid;
7
+
8
+ function getTimestamp(): string {
9
+ const now = new Date();
10
+ const date = now.toLocaleDateString("en-US");
11
+ const time = now.toLocaleTimeString("en-US");
12
+ return `${date}, ${time}`;
13
+ }
14
+
15
+ export class Logger {
16
+ private context: string;
17
+ private lastLogTime: number = Date.now();
18
+
19
+ constructor(context: string) {
20
+ this.context = context;
21
+ }
22
+
23
+ private formatMessage(level: string, message: string, levelColor: string): string {
24
+ const timestamp = getTimestamp();
25
+ const elapsed = Date.now() - this.lastLogTime;
26
+ this.lastLogTime = Date.now();
27
+
28
+ const harpyPid = `\x1b[32m[Harpy] ${pid}\x1b[0m`; // green
29
+ const timestampFormatted = `\x1b[37m${timestamp}\x1b[0m`; // white
30
+ const levelColored = `\x1b[${levelColor}m${level.padEnd(5)}\x1b[0m`;
31
+ const contextColored = `\x1b[33m[${this.context}]\x1b[0m`; // yellow
32
+ const messageColored = `\x1b[${levelColor}m${message}\x1b[0m`;
33
+ const elapsedColored = `\x1b[33m+${elapsed}ms\x1b[0m`; // yellow
34
+
35
+ return `${harpyPid} - ${timestampFormatted} ${levelColored} ${contextColored} ${messageColored} ${elapsedColored}`;
36
+ }
37
+
38
+ log(message: string): void {
39
+ console.log(this.formatMessage("LOG", message, "32")); // green
40
+ }
41
+
42
+ error(message: string): void {
43
+ console.error(this.formatMessage("ERROR", message, "31")); // red
44
+ }
45
+
46
+ warn(message: string): void {
47
+ console.warn(this.formatMessage("WARN", message, "33")); // yellow
48
+ }
49
+
50
+ verbose(message: string): void {
51
+ console.log(this.formatMessage("LOG", message, "36")); // cyan
52
+ }
53
+ }
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "child_process";
4
+ import { Logger } from "./logger";
5
+
6
+ const logger = new Logger("Server");
7
+
8
+ function startServer(): void {
9
+ logger.log("Starting production server...");
10
+
11
+ const serverProcess = spawn("node", ["dist/main.js"], {
12
+ stdio: "inherit",
13
+ shell: false,
14
+ cwd: process.cwd(),
15
+ });
16
+
17
+ serverProcess.on("error", (error) => {
18
+ logger.error(`Failed to start server: ${error.message}`);
19
+ process.exit(1);
20
+ });
21
+
22
+ serverProcess.on("exit", (code) => {
23
+ if (code !== 0) {
24
+ logger.error(`Server exited with code ${code}`);
25
+ process.exit(code || 1);
26
+ }
27
+ });
28
+
29
+ // Handle termination signals
30
+ process.on("SIGINT", () => {
31
+ logger.log("Shutting down server...");
32
+ serverProcess.kill("SIGINT");
33
+ });
34
+
35
+ process.on("SIGTERM", () => {
36
+ logger.log("Shutting down server...");
37
+ serverProcess.kill("SIGTERM");
38
+ });
39
+ }
40
+
41
+ startServer();
package/src/cli.ts CHANGED
@@ -10,12 +10,14 @@ const scripts: Record<string, string> = {
10
10
  "build-hydration": path.join(__dirname, "../scripts/build-hydration.ts"),
11
11
  "auto-wrap": path.join(__dirname, "../scripts/auto-wrap-exports.ts"),
12
12
  "build-styles": path.join(__dirname, "../scripts/build-page-styles.ts"),
13
+ build: path.join(__dirname, "../scripts/build.ts"),
14
+ start: path.join(__dirname, "../scripts/start.ts"),
13
15
  dev: path.join(__dirname, "../scripts/dev.ts"),
14
16
  };
15
17
 
16
18
  if (!command || !scripts[command]) {
17
19
  console.error("Usage: harpy <command>");
18
- console.error("Commands: build-hydration, auto-wrap, build-styles, dev");
20
+ console.error("Commands: build, start, dev, build-hydration, auto-wrap, build-styles");
19
21
  process.exit(1);
20
22
  }
21
23
 
@@ -28,6 +28,8 @@ export interface HarpyAppOptions {
28
28
  layout?: any;
29
29
  /** Folder containing built server assets (chunks) — defaults to `dist` */
30
30
  distDir?: string;
31
+ /** Optional folder containing public assets (favicon, manifest, etc.) */
32
+ publicDir?: string;
31
33
  }
32
34
 
33
35
  /**
@@ -43,7 +45,7 @@ export async function configureHarpyApp(
43
45
  app: NestFastifyApplication,
44
46
  opts: HarpyAppOptions = {},
45
47
  ) {
46
- const { layout, distDir = "dist" } = opts;
48
+ const { layout, distDir = "dist", publicDir } = opts;
47
49
 
48
50
  if (layout) {
49
51
  withJsxEngine(app, layout);
@@ -79,6 +81,16 @@ export async function configureHarpyApp(
79
81
  prefix: "/",
80
82
  decorateReply: false,
81
83
  });
84
+
85
+ // If publicDir is provided, register it as well for public assets
86
+ if (publicDir) {
87
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call
88
+ await fastify.register(fastifyStatic, {
89
+ root: path.join(process.cwd(), publicDir),
90
+ prefix: "/",
91
+ decorateReply: false,
92
+ });
93
+ }
82
94
  } else {
83
95
  // If the static plugin is not available, emit a warning and continue.
84
96
  // Consumers who need hydration chunk serving in production should add
package/src/index.ts CHANGED
@@ -33,3 +33,4 @@ export {
33
33
  getActiveItemIdFromIndex,
34
34
  getActiveItemIdFromManifest,
35
35
  } from "./client/getActiveItemId";
36
+ export { useI18n } from "./client/use-i18n";