npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@hepta-solutions/harpy-core

v0.4.7

Published

Harpy - A powerful NestJS + React/JSX SSR framework with automatic hydration and i18n support

Readme

@hepta-solutions/harpy-core

Core package for NestJS + React/JSX with server-side rendering and automatic client-side hydration.

Features

  • 🎯 JSX Engine - Render React components in NestJS controllers
  • 🔄 Auto Hydration - Client components marked with 'use client' automatically hydrate
  • Fast Builds - Optimized build pipeline with esbuild
  • 🚀 Performance Optimized - Shared vendor bundle (188KB) + tiny component chunks (1-3KB)
  • 📦 Zero Config - Works out of the box with NestJS
  • 🌐 I18n Support - Built-in internationalization with type-safe translations
  • 🍪 Cookie Management - Integrated with Fastify for session management
  • 🎨 CSS Optimization - Automatic minification with cssnano in production

Installation

npm install @hepta-solutions/harpy-core react react-dom
# or
yarn add @hepta-solutions/harpy-core react react-dom
# or
pnpm add @hepta-solutions/harpy-core react react-dom

Required peer dependencies:

  • @nestjs/common ^11.0.0
  • @nestjs/core ^11.0.0
  • @nestjs/platform-fastify ^11.0.0
  • react ^19.0.0
  • react-dom ^19.0.0

Quick Start

1. Set up the JSX engine in your main.ts

import "reflect-metadata"; // Required for NestJS
import { NestFactory } from "@nestjs/core";
import {
  FastifyAdapter,
  NestFastifyApplication,
} from "@nestjs/platform-fastify";
import { withJsxEngine } from "@hepta-solutions/harpy-core";
import { AppModule } from "./app.module";
import DefaultLayout from "./views/layout";
import * as path from "path";
import fastifyStatic from "@fastify/static";

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(
    AppModule,
    new FastifyAdapter(),
  );

  // Enable JSX rendering
  withJsxEngine(app, DefaultLayout);

  // Register static file serving
  const fastify = app.getHttpAdapter().getInstance();
  await fastify.register(fastifyStatic, {
    root: path.join(process.cwd(), "dist"),
    prefix: "/",
  });

  await app.listen({
    port: 3000,
    host: "0.0.0.0",
  });
}

bootstrap();

2. Create a layout component

// src/views/layout.tsx
import React from "react";
import { JsxLayoutProps } from "@hepta-solutions/harpy-core";

export default function Layout({
  children,
  meta,
  hydrationScripts,
}: JsxLayoutProps) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <title>{meta?.title || "My App"}</title>
        {meta?.description && (
          <meta name="description" content={meta.description} />
        )}
        <link rel="stylesheet" href="/styles/styles.css" />
      </head>
      <body>
        <main id="body">{children}</main>
        {/* Vendor bundle loads React/ReactDOM once */}
        {hydrationScripts?.vendorScript && (
          <script src={hydrationScripts.vendorScript} />
        )}
        {/* Component-specific hydration scripts */}
        {hydrationScripts?.componentScripts?.map((script) => (
          <script key={script.componentName} src={script.path} />
        ))}
      </body>
    </html>
  );
}

3. Create a controller with JSX rendering

import { Controller, Get } from "@nestjs/common";
import { JsxRender } from "@hepta-solutions/harpy-core";
import Homepage from "./views/homepage";

@Controller()
export class HomeController {
  @Get()
  @JsxRender(Homepage, {
    meta: {
      title: "Welcome",
      description: "My homepage",
    },
  })
  home() {
    return {
      message: "Hello World",
    };
  }
}

4. Create your page component

// src/features/home/views/homepage.tsx
import React from "react";
import Counter from "./counter";

export default function Homepage({ message }) {
  return (
    <div>
      <h1>{message}</h1>
      <Counter />
    </div>
  );
}

5. Create a client component

// src/features/home/views/counter.tsx
"use client";

import React from "react";

export default function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
}

API Reference

withJsxEngine(app, defaultLayout)

Sets up the JSX rendering engine on a NestJS Fastify application.

Parameters:

  • app - NestFastifyApplication instance
  • defaultLayout - React component to use as the default layout

@JsxRender(component, options?)

Decorator to render a React component from a controller method.

Parameters:

  • component - React component to render
  • options - Rendering options
    • meta - Meta tags for the page (title, description, og tags, etc.)
    • layout - Custom layout component (optional)

autoWrapClientComponent(Component, componentName)

Wraps a component for automatic hydration. Used internally by the build process.

Build Scripts

The package includes CLI commands for building:

# Build hydration bundles
harpy build-hydration

# Auto-wrap client components
harpy auto-wrap

# Build styles
harpy build-styles

# Development mode
harpy dev

How It Works

  1. Server-Side Rendering: Components are rendered to HTML on the server
  2. Component Registration: Client components (marked with 'use client') register themselves during SSR
  3. Auto-Wrapping: Build scripts automatically wrap client components for hydration
  4. Vendor Bundle Optimization: React and ReactDOM are bundled once (188KB) and shared across all components
  5. Client Bundling: Client components are bundled separately with esbuild (1-3KB each)
  6. Hydration: Client bundles load React from the shared vendor and hydrate the SSR'd HTML

Performance Optimizations

The framework implements several performance optimizations:

  • Shared Vendor Bundle: React (19.x) and ReactDOM are bundled once (188KB minified) instead of being duplicated in each component
  • Tiny Component Chunks: Individual components are only 1-3KB each (97% reduction compared to bundling React in each)
  • Tree Shaking: Unused code is automatically removed during production builds
  • CSS Minification: Stylesheets are automatically minified with cssnano in production
  • Production Mode: process.env.NODE_ENV is properly set to enable React optimizations

Internationalization (i18n)

Built-in support for multi-language applications:

// app.module.ts
import { Module } from "@nestjs/common";
import { I18nModule } from "@hepta-solutions/harpy-i18n";

@Module({
  imports: [
    I18nModule.forRoot({
      defaultLocale: "en",
      supportedLocales: ["en", "fr", "ar"],
      dictionaries: {
        en: () => import("./dictionaries/en.json"),
        fr: () => import("./dictionaries/fr.json"),
        ar: () => import("./dictionaries/ar.json"),
      },
    }),
  ],
})
export class AppModule {}

Using translations in components:

// Client component
"use client";
import { useI18n } from "@hepta-solutions/harpy-core/client";

export default function MyComponent() {
  const { t, locale, setLocale } = useI18n();

  return (
    <div>
      <h1>{t("welcome.title")}</h1>
      <button onClick={() => setLocale("fr")}>Switch to French</button>
    </div>
  );
}

Using translations in controllers (server-side):

import { Controller, Get } from "@nestjs/common";
import { JsxRender } from "@hepta-solutions/harpy-core";
import { CurrentLocale, t } from "@hepta-solutions/harpy-i18n";
import { getDictionary } from "./i18n/get-dictionary";

@Controller()
export class HomeController {
  @Get()
  @JsxRender(Homepage)
  async home(@CurrentLocale() locale: string) {
    const dict = await getDictionary(locale);

    return {
      title: t(dict, "home.title"),
      message: t(dict, "home.welcome"),
      dict,
      locale,
    };
  }
}

TypeScript Configuration

Make sure your tsconfig.json includes:

{
  "compilerOptions": {
    "jsx": "react",
    "esModuleInterop": true,
    "paths": {
      "@/*": ["src/*"]
    }
  }
}

License

MIT