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:
| Field | Type | Default | Description |
|---|---|---|---|
components | string | "./src/components" | Path to the components directory |
entry | string | "App" | Name of the entry component |
port | number | 3000 | Dev server port |
outDir | string | "dist" | Production build output directory |
Fallback Resolution
The CLI resolves configuration in this order:
hypen.config.ts(TypeScript, recommended)hypen.json(JSON alternative)- 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.
Folder-Based (Recommended)
components/
└── Counter/
├── component.hypen # UI template
└── component.ts # Module (optional)Component name: derived from the folder name → Counter
Index-Based
components/
└── Counter/
├── index.hypen # UI template
└── index.ts # Module (optional)Component name: derived from the folder name → Counter
Sibling Files
components/
├── Counter.hypen # UI template
└── Counter.ts # Module (optional)Component name: derived from the filename → Counter
Single-File
components/
└── Counter.ts # Module with inline template via .ui()Component name: derived from the filename → Counter
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.hypenEach 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
.tsmodule file has state and can handle actions. - A component without a
.tsmodule 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.tsimport 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
- CLI Reference — CLI commands and options
- Remote Apps — Building server-driven UIs
- TypeScript Server — RemoteServer API reference