HypenHypen
Tooling

Configuration

Configure your Hypen project with hypen.config.ts

Configuration

Every Hypen project uses a hypen.config.ts file in the project root to configure the CLI, dev server, and build system.

Config File

Create a hypen.config.ts in your project root:

export default {
  // Path to the components directory (relative to project root)
  components: "./src/components",

  // Entry component name — the component rendered on startup
  entry: "App",

  // Dev server port
  port: 3000,

  // Production build output directory
  outDir: "dist",
};

All fields:

FieldTypeDefaultDescription
componentsstring"./src/components"Path to the components directory
entrystring"App"Name of the entry component
portnumber3000Dev server port
outDirstring"dist"Production build output directory

Fallback Resolution

The CLI resolves configuration in this order:

  1. hypen.config.ts (TypeScript, recommended)
  2. hypen.json (JSON alternative)
  3. Built-in defaults

Component Discovery

The CLI automatically discovers components by scanning the components directory. Discovery is recursive by default — components in subdirectories are found automatically.

Naming Patterns

Four naming conventions are supported. All are enabled by default.

components/
└── Counter/
    ├── component.hypen    # UI template
    └── component.ts       # Module (optional)

Component name: derived from the folder nameCounter

Index-Based

components/
└── Counter/
    ├── index.hypen        # UI template
    └── index.ts           # Module (optional)

Component name: derived from the folder nameCounter

Sibling Files

components/
├── Counter.hypen          # UI template
└── Counter.ts             # Module (optional)

Component name: derived from the filenameCounter

Single-File

components/
└── Counter.ts             # Module with inline template via .ui()

Component name: derived from the filenameCounter

The .ts file must export a module with an inline template:

import { app, hypen } from "@hypen-space/core";

export default app
  .defineState<{ count: number }>({ count: 0 })
  .onAction("increment", ({ state }) => { state.count++; })
  .ui(hypen`
    module Counter {
      Text("Count: ${state.count}")
      Button { Text("+") }.onClick(@actions.increment)
    }
  `)
  .build();

Nested Components

Since discovery is recursive, you can organize components in subdirectories:

components/
├── App/
│   ├── component.hypen
│   └── component.ts
├── pages/
│   ├── Home/
│   │   ├── component.hypen
│   │   └── component.ts
│   └── Settings/
│       ├── component.hypen
│       └── component.ts
└── shared/
    ├── Header/
    │   └── component.hypen
    └── Footer/
        └── component.hypen

Each component is identified by its immediate folder name, not the full path. So pages/Home/component.hypen registers as Home, and shared/Header/component.hypen registers as Header.

Stateless vs Stateful Components

  • A component with a .ts module file has state and can handle actions.
  • A component without a .ts module file is stateless — it renders pure UI.

Using Components in Templates

Once discovered, components are available by name in any .hypen template:

Column {
  Header
  Router {
    Route(path: "/") { Home }
    Route(path: "/settings") { Settings }
  }
  Footer
}

You can also use explicit imports:

import Header from "../shared/Header/component.hypen"

module App {
  Column {
    Header()
    Text("Welcome")
  }
}

Integration with Existing Backends

Hypen doesn't require the CLI to run. If you have an existing backend (Fastify, Express, etc.), you can use the engine directly via WebSocket with the Remote UI protocol.

Directory-Based Discovery with .source()

The simplest way to use Hypen with an existing backend is .source(). It discovers all components from a directory (recursively) and auto-resolves imports in templates:

src/
├── index.ts              # Your Fastify server
└── apps/
    └── chat/
        ├── Chat/
        │   ├── component.hypen    # Entry template (imports Settings, History)
        │   └── component.ts       # Chat module
        └── screens/
            ├── Settings/
            │   ├── component.hypen
            │   └── component.ts
            └── History/
                ├── component.hypen
                └── component.ts
import Fastify from "fastify";
import { RemoteServer } from "@hypen-space/server/remote";
import chatModule from "./apps/chat/Chat/component.js";

// Your existing Fastify app
const app = Fastify();
app.get("/api/messages", async () => ({ messages: [] }));

// Hypen discovers Chat, Settings, History automatically
await new RemoteServer()
  .source("./apps/chat")
  .module("Chat", chatModule)
  .listen(3001);

await app.listen({ port: 3000 });

Now Chat/component.hypen can import and reference sub-components freely:

import Settings from "../screens/Settings/component.hypen"
import History from "../screens/History/component.hypen"

module Chat {
  Router {
    Route(path: "/settings") {
      Settings()
    }
    Route(path: "/history") {
      History()
    }
  }
}

All components under ./apps/chat are discovered recursively and wired into the engine's component resolver. No manual registration needed.

Inline UI (Single Component)

For simple single-component apps, you can skip .source() and pass the template directly:

import { RemoteServer } from "@hypen-space/server/remote";
import counterModule from "./components/Counter/component.js";
import { readFileSync } from "fs";

new RemoteServer()
  .module("Counter", counterModule)
  .ui(readFileSync("./components/Counter/component.hypen", "utf-8"))
  .listen(3001);

Note: with inline .ui(), any imports or component references in the template will not be resolved since the server has no source directory to discover from.

Remote Client

On the browser side, connect to the remote server:

import { RemoteEngine } from "@hypen-space/core/remote";
import { DOMRenderer } from "@hypen-space/web";

const engine = new RemoteEngine("ws://localhost:3001", {
  autoReconnect: true,
});

const renderer = new DOMRenderer(document.getElementById("app"));
engine.onPatches((patches) => renderer.applyPatches(patches));
await engine.connect();

The remote protocol supports session persistence, reconnection with state restoration, and works with any renderer (DOM, Canvas, Android, iOS).

Programmatic Discovery (No CLI)

You can also use the discovery API directly in your own server code:

import { discoverComponents, loadDiscoveredComponents } from "@hypen-space/server";

const discovered = await discoverComponents("./src/components");
const components = await loadDiscoveredComponents(discovered);

// Register with your own rendering pipeline
for (const [name, { module, template }] of components) {
  console.log(`Found component: ${name}`);
}

See Also