1056 lines
22 KiB
Markdown
1056 lines
22 KiB
Markdown
# Guide to Writing Hype Apps
|
|
|
|
Hype is a thin, opinionated wrapper around [Hono](https://hono.dev) for fast prototyping with Bun. It gives you file-based routing, automatic TypeScript transpilation, SSE, and a default HTML5 layout — all without a build step.
|
|
|
|
Since `Hype extends Hono`, every Hono API (`get`, `post`, `use`, `on`, etc.) works out of the box.
|
|
|
|
## Table of Contents
|
|
|
|
- [Getting Started](#getting-started)
|
|
- [Project Structure](#project-structure)
|
|
- [SSR Apps](#ssr-apps)
|
|
- [SPA Apps](#spa-apps)
|
|
- [Routing](#routing)
|
|
- [Layouts](#layouts)
|
|
- [Pages](#pages)
|
|
- [Client-Side JavaScript](#client-side-javascript)
|
|
- [Styling](#styling)
|
|
- [Server-Sent Events (SSE)](#server-sent-events-sse)
|
|
- [Custom API Routes](#custom-api-routes)
|
|
- [Sub-Routers](#sub-routers)
|
|
- [Static Files](#static-files)
|
|
- [Configuration](#configuration)
|
|
- [Environment Variables](#environment-variables)
|
|
- [Path Aliases](#path-aliases)
|
|
- [Utility Functions](#utility-functions)
|
|
- [Recipes](#recipes)
|
|
|
|
---
|
|
|
|
## Getting Started
|
|
|
|
```sh
|
|
# add hype to your project
|
|
bun add @because/hype
|
|
|
|
# create your server entry point
|
|
mkdir -p src/server src/pages src/css src/client pub
|
|
```
|
|
|
|
Add scripts to `package.json`:
|
|
|
|
```json
|
|
{
|
|
"scripts": {
|
|
"start": "bun run src/server/index.ts",
|
|
"dev": "bun run --hot src/server/index.ts"
|
|
}
|
|
}
|
|
```
|
|
|
|
Add the required `tsconfig.json`:
|
|
|
|
```json
|
|
{
|
|
"compilerOptions": {
|
|
"lib": ["ESNext", "DOM"],
|
|
"target": "ESNext",
|
|
"module": "Preserve",
|
|
"moduleDetection": "force",
|
|
"jsx": "react-jsx",
|
|
"jsxImportSource": "hono/jsx",
|
|
"allowJs": true,
|
|
"moduleResolution": "bundler",
|
|
"allowImportingTsExtensions": true,
|
|
"verbatimModuleSyntax": true,
|
|
"noEmit": true,
|
|
"strict": true,
|
|
"skipLibCheck": true,
|
|
"noFallthroughCasesInSwitch": true,
|
|
"noUncheckedIndexedAccess": true,
|
|
"noImplicitOverride": true,
|
|
"baseUrl": ".",
|
|
"paths": {
|
|
"$*": ["src/server/*"],
|
|
"#*": ["src/client/*"],
|
|
"@*": ["src/shared/*"]
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
Create your server:
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
Create your first page:
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
export default () => (
|
|
<section>
|
|
<h1>Hello, world!</h1>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
Run it:
|
|
|
|
```sh
|
|
bun dev
|
|
```
|
|
|
|
That's it. You have a server-rendered page at `http://localhost:3000`.
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
.
|
|
├── package.json
|
|
├── tsconfig.json
|
|
├── pub/ # Static files (served as-is at /)
|
|
│ └── img/
|
|
│ └── logo.png # => /img/logo.png
|
|
├── src/
|
|
│ ├── server/
|
|
│ │ └── index.ts # Server entry point
|
|
│ ├── pages/
|
|
│ │ ├── index.tsx # => GET /
|
|
│ │ ├── about.tsx # => GET /about
|
|
│ │ └── _layout.tsx # Custom layout (optional)
|
|
│ ├── client/
|
|
│ │ └── main.ts # Client JS (auto-included by default layout)
|
|
│ ├── shared/
|
|
│ │ └── types.ts # Shared between server and client
|
|
│ └── css/
|
|
│ └── main.css # App CSS (auto-included by default layout)
|
|
```
|
|
|
|
- `src/pages/` — SSR pages, one file per route
|
|
- `src/client/` — Client-side TypeScript, transpiled and bundled on demand
|
|
- `src/shared/` — Isomorphic code, available to both server and client
|
|
- `src/css/` — Stylesheets
|
|
- `src/server/` — Server-only code
|
|
- `pub/` — Static assets served directly
|
|
|
|
---
|
|
|
|
## SSR Apps
|
|
|
|
SSR is the default mode. Pages in `src/pages/` are server-rendered on every request using Hono's JSX engine and wrapped in a layout.
|
|
|
|
### Minimal SSR app
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
export default () => (
|
|
<section>
|
|
<h1>Welcome</h1>
|
|
<p>This is server-rendered HTML.</p>
|
|
<a href="/about">About</a>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
```tsx
|
|
// src/pages/about.tsx
|
|
export default () => (
|
|
<section>
|
|
<h1>About</h1>
|
|
<p>This website was made using futuristic internet technologies.</p>
|
|
<a href="/"><= Back</a>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
The default layout automatically includes `src/css/main.css` and `src/client/main.ts`, wraps your page content in `<html>`, `<head>`, and `<body><main>...</main></body>`.
|
|
|
|
### Accessing the request
|
|
|
|
Page components receive `c` (the Hono context) and `req` (the Hono request) as props:
|
|
|
|
```tsx
|
|
// src/pages/greet.tsx
|
|
export default ({ req }) => (
|
|
<section>
|
|
<h1>Hello, {req.query('name') ?? 'stranger'}!</h1>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
Visit `/greet?name=Chris` to see `Hello, Chris!`.
|
|
|
|
For the full Hono context:
|
|
|
|
```tsx
|
|
// src/pages/debug.tsx
|
|
export default ({ c, req }) => {
|
|
const ua = req.header('user-agent')
|
|
return (
|
|
<section>
|
|
<h1>Request Info</h1>
|
|
<p>URL: {req.url}</p>
|
|
<p>User-Agent: {ua}</p>
|
|
</section>
|
|
)
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## SPA Apps
|
|
|
|
For a single-page app with client-side rendering, disable the default layout and provide your own HTML shell:
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype({ layout: false })
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
export default () => (
|
|
<html lang="en">
|
|
<head>
|
|
<title>My SPA</title>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="color-scheme" content="light dark" />
|
|
<link href="/css/main.css" rel="stylesheet" />
|
|
</head>
|
|
<body>
|
|
<div id="root" />
|
|
<script src="/client/app.js" type="module" />
|
|
</body>
|
|
</html>
|
|
)
|
|
```
|
|
|
|
```tsx
|
|
// src/client/app.tsx
|
|
import { render } from 'hono/jsx/dom'
|
|
import { useState } from 'hono/jsx/dom'
|
|
|
|
function App() {
|
|
const [count, setCount] = useState(0)
|
|
|
|
return (
|
|
<>
|
|
<h1>My SPA</h1>
|
|
<p>Count: {count}</p>
|
|
<button onClick={() => setCount(c => c + 1)}>+</button>
|
|
<button onClick={() => setCount(c => c && c - 1)}>-</button>
|
|
</>
|
|
)
|
|
}
|
|
|
|
render(<App />, document.getElementById('root')!)
|
|
```
|
|
|
|
The client-side app uses `hono/jsx/dom` for React-like rendering with hooks (`useState`, `useEffect`, etc.). Hype automatically transpiles and bundles `src/client/app.tsx` when the browser requests `/client/app.js`.
|
|
|
|
### Cache-busting in SPAs
|
|
|
|
Use the git hash to bust asset caches:
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
import { $ } from 'bun'
|
|
|
|
const GIT_HASH = await $`git rev-parse --short HEAD`.text().then(s => s.trim()).catch(() => 'dev')
|
|
|
|
export default () => (
|
|
<html lang="en">
|
|
<head>
|
|
<link href={`/css/main.css?${GIT_HASH}`} rel="stylesheet" />
|
|
</head>
|
|
<body>
|
|
<div id="root" />
|
|
<script src={`/client/app.js?${GIT_HASH}`} type="module" />
|
|
</body>
|
|
</html>
|
|
)
|
|
```
|
|
|
|
### Passing server data to the client
|
|
|
|
Inject globals via an inline script:
|
|
|
|
```tsx
|
|
<script dangerouslySetInnerHTML={{
|
|
__html: `
|
|
window.GIT_HASH = '${GIT_HASH}';
|
|
window.API_URL = '${process.env.API_URL}';
|
|
`
|
|
}} />
|
|
```
|
|
|
|
---
|
|
|
|
## Routing
|
|
|
|
Hype supports two routing layers that work together.
|
|
|
|
### File-based routing
|
|
|
|
Files in `src/pages/` are automatically mapped to GET routes:
|
|
|
|
| File | URL |
|
|
|------|-----|
|
|
| `src/pages/index.tsx` | `/` |
|
|
| `src/pages/about.tsx` | `/about` |
|
|
| `src/pages/contact.tsx` | `/contact` |
|
|
|
|
Files prefixed with `_` are private and won't be served (returns 404). This is used for layouts and other internal files.
|
|
|
|
### Custom routes
|
|
|
|
Since Hype extends Hono, you can define any route directly:
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
// JSON API
|
|
app.get('/api/users/:id', (c) => {
|
|
return c.json({ id: c.req.param('id'), name: 'Chris' })
|
|
})
|
|
|
|
// Form handling
|
|
app.post('/api/contact', async (c) => {
|
|
const body = await c.req.parseBody()
|
|
console.log('Message:', body.message)
|
|
return c.redirect('/thanks')
|
|
})
|
|
|
|
// Custom HTML
|
|
app.get('/custom', (c) => {
|
|
return c.html('<h1>Custom page</h1>')
|
|
})
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
Custom routes are defined before the file-based routes, so they take priority.
|
|
|
|
---
|
|
|
|
## Layouts
|
|
|
|
### Default layout
|
|
|
|
By default, Hype wraps every page in a simple HTML5 layout that includes:
|
|
|
|
- `<meta charset="utf-8">`
|
|
- `<meta name="viewport">` for responsive design
|
|
- `<meta name="color-scheme" content="light dark">` for automatic dark mode
|
|
- `<link>` to `/css/main.css` (your `src/css/main.css`)
|
|
- `<script>` loading `/client/main.ts` (your `src/client/main.ts`)
|
|
- Content wrapped in `<body><main>...</main></body>`
|
|
|
|
Optionally includes the CSS reset and/or Pico CSS based on your config.
|
|
|
|
### Custom layout
|
|
|
|
Create `src/pages/_layout.tsx` to replace the default layout:
|
|
|
|
```tsx
|
|
// src/pages/_layout.tsx
|
|
import type { FC } from 'hono/jsx'
|
|
|
|
const Layout: FC = ({ children, title, props }) => (
|
|
<html lang="en">
|
|
<head>
|
|
<title>{title ?? 'My App'}</title>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
|
|
<link href="/css/main.css" rel="stylesheet" />
|
|
<script src="/client/main.ts" type="module"></script>
|
|
</head>
|
|
<body>
|
|
<nav>
|
|
<a href="/">Home</a>
|
|
<a href="/about">About</a>
|
|
</nav>
|
|
<main>{children}</main>
|
|
<footer>Built with Hype</footer>
|
|
</body>
|
|
</html>
|
|
)
|
|
|
|
export default Layout
|
|
```
|
|
|
|
The layout receives:
|
|
- `children` — the rendered page content
|
|
- `title` — page title (defaults to `'hype'`)
|
|
- `props` — the `HypeProps` passed to the constructor (useful for conditional CSS)
|
|
|
|
### No layout
|
|
|
|
Disable the layout entirely for full control (used in SPA mode):
|
|
|
|
```ts
|
|
const app = new Hype({ layout: false })
|
|
```
|
|
|
|
---
|
|
|
|
## Pages
|
|
|
|
Pages are `.tsx` files in `src/pages/` that export a default function (or raw JSX).
|
|
|
|
### Function export (recommended)
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
export default ({ c, req }) => (
|
|
<section>
|
|
<h1>Home</h1>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
### Static JSX export
|
|
|
|
```tsx
|
|
// src/pages/about.tsx
|
|
export default (
|
|
<section>
|
|
<h1>About</h1>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
### Async data in pages
|
|
|
|
Since pages render on the server, you can use top-level await:
|
|
|
|
```tsx
|
|
// src/pages/users.tsx
|
|
const users = await fetch('https://api.example.com/users').then(r => r.json())
|
|
|
|
export default () => (
|
|
<section>
|
|
<h1>Users</h1>
|
|
<ul>
|
|
{users.map(u => <li>{u.name}</li>)}
|
|
</ul>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
Note: top-level data is fetched once at import time and cached. For per-request data, use the `c` context:
|
|
|
|
```tsx
|
|
// src/pages/profile.tsx
|
|
export default async ({ c, req }) => {
|
|
const userId = req.query('id')
|
|
const user = await fetch(`https://api.example.com/users/${userId}`).then(r => r.json())
|
|
|
|
return (
|
|
<section>
|
|
<h1>{user.name}</h1>
|
|
</section>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Private pages
|
|
|
|
Prefix a file with `_` to prevent it from being served:
|
|
|
|
```
|
|
src/pages/_layout.tsx # not a route — used as the layout
|
|
src/pages/_helpers.tsx # not a route — internal helpers
|
|
src/pages/index.tsx # GET /
|
|
```
|
|
|
|
---
|
|
|
|
## Client-Side JavaScript
|
|
|
|
### Automatic transpilation
|
|
|
|
TypeScript files in `src/client/` and `src/shared/` are automatically transpiled and bundled by Bun when requested by the browser. The URL maps directly to the file path:
|
|
|
|
| File | URL |
|
|
|------|-----|
|
|
| `src/client/main.ts` | `/client/main.ts` |
|
|
| `src/client/app.tsx` | `/client/app.js` |
|
|
| `src/shared/utils.ts` | `/shared/utils.ts` |
|
|
|
|
You can request `.ts` or `.js` extensions — Hype resolves `.ts` and `.tsx` files automatically.
|
|
|
|
### Module imports
|
|
|
|
Client-side files can import from each other using relative paths:
|
|
|
|
```ts
|
|
// src/client/main.ts
|
|
import { initBurger } from './burger'
|
|
|
|
initBurger()
|
|
```
|
|
|
|
```ts
|
|
// src/client/burger.ts
|
|
export function initBurger() {
|
|
document.addEventListener('click', (ev) => {
|
|
const el = (ev?.target as HTMLElement).closest('.burger') as HTMLImageElement
|
|
if (!el) return
|
|
el.src = '/img/bite.png'
|
|
})
|
|
}
|
|
```
|
|
|
|
Imports are bundled — the full dependency graph is included in the output, so the browser only needs one request.
|
|
|
|
### The default layout auto-includes `main.ts`
|
|
|
|
When using the default layout, `src/client/main.ts` is automatically loaded as a module. Just create the file and it works.
|
|
|
|
---
|
|
|
|
## Styling
|
|
|
|
### External CSS
|
|
|
|
Put your styles in `src/css/main.css`. The default layout auto-includes it:
|
|
|
|
```css
|
|
/* src/css/main.css */
|
|
section {
|
|
max-width: 500px;
|
|
margin: 0 auto;
|
|
}
|
|
```
|
|
|
|
You can also serve additional CSS files from `src/css/`:
|
|
|
|
```html
|
|
<link href="/css/components.css" rel="stylesheet" />
|
|
```
|
|
|
|
### Pico CSS
|
|
|
|
Enable the bundled [Pico CSS](https://picocss.com) for classless styling:
|
|
|
|
```ts
|
|
const app = new Hype({ pico: true })
|
|
```
|
|
|
|
Or include it in a custom layout:
|
|
|
|
```html
|
|
<link href="/css/pico.css" rel="stylesheet" />
|
|
```
|
|
|
|
### CSS Reset
|
|
|
|
Enable the bundled CSS reset (Josh W. Comeau's reset):
|
|
|
|
```ts
|
|
const app = new Hype({ reset: true })
|
|
```
|
|
|
|
Or include it in a custom layout:
|
|
|
|
```html
|
|
<link href="/css/reset.css" rel="stylesheet" />
|
|
```
|
|
|
|
### Combining options
|
|
|
|
```ts
|
|
const app = new Hype({ pico: true, reset: true })
|
|
```
|
|
|
|
---
|
|
|
|
## Server-Sent Events (SSE)
|
|
|
|
Hype provides `app.sse()` for streaming data to the browser.
|
|
|
|
### Server
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
// Stream the current time every second
|
|
app.sse('/api/time', (send) => {
|
|
send({ time: Date.now() })
|
|
const interval = setInterval(() => send({ time: Date.now() }), 1000)
|
|
return () => clearInterval(interval) // cleanup on disconnect
|
|
})
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
The `send` function:
|
|
- Automatically `JSON.stringify`s objects; strings are sent as-is
|
|
- Accepts an optional second argument for named events: `send(data, 'eventName')`
|
|
|
|
The handler receives `(send, c)` where `c` is the Hono context:
|
|
|
|
```ts
|
|
app.sse('/api/user-events', (send, c) => {
|
|
const userId = c.req.query('userId')
|
|
// subscribe to user-specific events...
|
|
})
|
|
```
|
|
|
|
Return a cleanup function to handle client disconnection.
|
|
|
|
### Client
|
|
|
|
```tsx
|
|
// src/pages/sse.tsx
|
|
export default () => (
|
|
<section>
|
|
<h1>SSE Demo</h1>
|
|
<div id="time" style="font-size: 2em; font-family: monospace;"></div>
|
|
|
|
<script src="/client/main.ts" type="module"></script>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
```ts
|
|
// src/client/main.ts
|
|
const timeEl = document.getElementById('time')
|
|
const events = new EventSource('/api/time')
|
|
|
|
events.onmessage = (e) => {
|
|
const data = JSON.parse(e.data)
|
|
timeEl!.textContent = new Date(data.time).toLocaleTimeString()
|
|
}
|
|
|
|
events.onerror = () => {
|
|
timeEl!.textContent = 'Disconnected'
|
|
}
|
|
```
|
|
|
|
### Named events
|
|
|
|
```ts
|
|
// Server
|
|
app.sse('/api/feed', (send) => {
|
|
send({ type: 'user_joined', name: 'Chris' }, 'activity')
|
|
send({ message: 'Hello everyone!' }, 'chat')
|
|
})
|
|
```
|
|
|
|
```js
|
|
// Client
|
|
const events = new EventSource('/api/feed')
|
|
|
|
events.addEventListener('activity', (e) => {
|
|
console.log('Activity:', JSON.parse(e.data))
|
|
})
|
|
|
|
events.addEventListener('chat', (e) => {
|
|
console.log('Chat:', JSON.parse(e.data))
|
|
})
|
|
```
|
|
|
|
### Test with curl
|
|
|
|
```sh
|
|
curl -N http://localhost:3000/api/time
|
|
```
|
|
|
|
---
|
|
|
|
## Custom API Routes
|
|
|
|
Since Hype extends Hono, use any Hono routing method:
|
|
|
|
```ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
// GET with params
|
|
app.get('/api/users/:id', (c) => {
|
|
return c.json({ id: c.req.param('id') })
|
|
})
|
|
|
|
// POST with body parsing
|
|
app.post('/api/users', async (c) => {
|
|
const body = await c.req.json()
|
|
return c.json({ created: true, name: body.name }, 201)
|
|
})
|
|
|
|
// Form submission
|
|
app.post('/contact', async (c) => {
|
|
const form = await c.req.parseBody()
|
|
console.log(form.email, form.message)
|
|
return c.redirect('/')
|
|
})
|
|
|
|
// Delete
|
|
app.delete('/api/users/:id', (c) => {
|
|
return c.json({ deleted: true })
|
|
})
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
### Redirect back
|
|
|
|
Use `redirectBack()` to redirect to the referrer:
|
|
|
|
```ts
|
|
import { Hype, redirectBack } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
app.post('/api/like', async (c) => {
|
|
// ... handle the like
|
|
return redirectBack(c, '/') // falls back to '/' if no referrer
|
|
})
|
|
```
|
|
|
|
---
|
|
|
|
## Sub-Routers
|
|
|
|
Use `Hype.router()` to create sub-routers without duplicate middleware:
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype } from '@because/hype'
|
|
import api from './api'
|
|
|
|
const app = new Hype()
|
|
app.route('/api', api)
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
```ts
|
|
// src/server/api.ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const api = Hype.router()
|
|
|
|
api.get('/users', (c) => c.json([{ id: 1, name: 'Chris' }]))
|
|
api.get('/users/:id', (c) => c.json({ id: c.req.param('id') }))
|
|
api.post('/users', async (c) => {
|
|
const body = await c.req.json()
|
|
return c.json(body, 201)
|
|
})
|
|
|
|
export default api
|
|
```
|
|
|
|
`Hype.router()` creates a Hype instance that skips middleware registration (no duplicate logging, static file serving, etc.).
|
|
|
|
---
|
|
|
|
## Static Files
|
|
|
|
Files in `pub/` are served at the root URL:
|
|
|
|
```
|
|
pub/img/logo.png => /img/logo.png
|
|
pub/favicon.ico => /favicon.ico
|
|
pub/robots.txt => /robots.txt
|
|
```
|
|
|
|
Use them in pages:
|
|
|
|
```tsx
|
|
export default () => (
|
|
<section>
|
|
<img src="/img/logo.png" width="200" />
|
|
</section>
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### Constructor options
|
|
|
|
```ts
|
|
const app = new Hype({
|
|
pico: true, // Include Pico CSS (default: false)
|
|
reset: true, // Include CSS reset (default: false)
|
|
prettyHTML: true, // Pretty-print HTML in dev (default: true in dev, false in prod)
|
|
layout: true, // Wrap pages in layout (default: true)
|
|
logging: true, // HTTP request logging (default: true)
|
|
ok: true, // Add GET /ok healthcheck (default: false)
|
|
})
|
|
```
|
|
|
|
### `app.defaults`
|
|
|
|
Always export `app.defaults` as the default export. It returns a Bun.serve-compatible config object:
|
|
|
|
```ts
|
|
export default app.defaults
|
|
// => { port, fetch, idleTimeout: 255 }
|
|
```
|
|
|
|
This triggers lazy route registration and auto-port selection. Bun picks it up as the server config when the file is the entry point.
|
|
|
|
---
|
|
|
|
## Environment Variables
|
|
|
|
| Variable | Effect |
|
|
|----------|--------|
|
|
| `PORT` | Server port (default: `3000`) |
|
|
| `NODE_ENV` | Set to `production` to disable auto-port, HTML prettification, and enable transpile caching |
|
|
| `NO_AUTOPORT` | Disable auto-port selection even in dev |
|
|
|
|
In dev mode, if port 3000 is busy, Hype automatically tries 3001, 3002, etc. (up to 100 attempts).
|
|
|
|
---
|
|
|
|
## Path Aliases
|
|
|
|
The recommended `tsconfig.json` defines three path aliases:
|
|
|
|
| Alias | Maps to | Use for |
|
|
|-------|---------|---------|
|
|
| `$*` | `src/server/*` | Server-only imports |
|
|
| `#*` | `src/client/*` | Client-only imports |
|
|
| `@*` | `src/shared/*` | Isomorphic imports |
|
|
|
|
```ts
|
|
// In server code:
|
|
import { db } from '$db' // => src/server/db.ts
|
|
|
|
// In client code:
|
|
import { format } from '@utils' // => src/shared/utils.ts
|
|
```
|
|
|
|
---
|
|
|
|
## Utility Functions
|
|
|
|
Hype exports a collection of helpers from `@because/hype` (or `@because/hype/utils`):
|
|
|
|
### Randomness
|
|
|
|
```ts
|
|
import { rand, randRange, randItem, randIndex, shuffle, weightedRand, randomId } from '@because/hype'
|
|
|
|
rand() // 1 or 2 (coin flip)
|
|
rand(6) // 1-6 (roll a die)
|
|
rand(20) // 1-20 (d20)
|
|
randRange(5, 10) // 5-10 inclusive
|
|
randItem(['a', 'b', 'c']) // random element
|
|
randIndex(['a', 'b', 'c']) // random index (0, 1, or 2)
|
|
shuffle([1, 2, 3, 4, 5]) // shuffled copy
|
|
weightedRand() // 1-10, lower numbers more likely
|
|
randomId() // e.g. "k7x2m1"
|
|
```
|
|
|
|
### Arrays
|
|
|
|
```ts
|
|
import { times, unique } from '@because/hype'
|
|
|
|
times(5) // [1, 2, 3, 4, 5]
|
|
unique([1, 1, 2, 2, 3, 3]) // [1, 2, 3]
|
|
```
|
|
|
|
### Colors
|
|
|
|
```ts
|
|
import { lightenColor, darkenColor } from '@because/hype'
|
|
|
|
lightenColor('#3498db', 0.5) // blend halfway to white
|
|
darkenColor('#3498db', 0.5) // blend halfway to black
|
|
```
|
|
|
|
### Strings
|
|
|
|
```ts
|
|
import { capitalize } from '@because/hype'
|
|
|
|
capitalize('hello') // "Hello"
|
|
```
|
|
|
|
### Dark mode detection (client-side)
|
|
|
|
```ts
|
|
import { isDarkMode } from '@because/hype'
|
|
|
|
if (isDarkMode()) {
|
|
// user prefers dark mode
|
|
}
|
|
```
|
|
|
|
### Transpilation
|
|
|
|
```ts
|
|
import { transpile } from '@because/hype'
|
|
|
|
const js = await transpile('./src/client/app.tsx')
|
|
// => bundled ESM JavaScript string
|
|
```
|
|
|
|
---
|
|
|
|
## Recipes
|
|
|
|
### Form with POST handler
|
|
|
|
```ts
|
|
// src/server/index.ts
|
|
import { Hype, redirectBack } from '@because/hype'
|
|
|
|
const app = new Hype()
|
|
|
|
const messages: string[] = []
|
|
|
|
app.post('/api/message', async (c) => {
|
|
const { message } = await c.req.parseBody()
|
|
if (typeof message === 'string') messages.push(message)
|
|
return redirectBack(c)
|
|
})
|
|
|
|
app.get('/api/messages', (c) => c.json(messages))
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
```tsx
|
|
// src/pages/index.tsx
|
|
export default () => (
|
|
<section>
|
|
<h1>Guestbook</h1>
|
|
<form method="POST" action="/api/message">
|
|
<input name="message" placeholder="Leave a message" required />
|
|
<button type="submit">Send</button>
|
|
</form>
|
|
</section>
|
|
)
|
|
```
|
|
|
|
### Healthcheck endpoint
|
|
|
|
```ts
|
|
const app = new Hype({ ok: true })
|
|
// GET /ok => "ok" (200)
|
|
```
|
|
|
|
### SSR with Pico CSS
|
|
|
|
```ts
|
|
const app = new Hype({ pico: true, reset: true })
|
|
```
|
|
|
|
All your pages instantly get clean, classless styling. Just write semantic HTML.
|
|
|
|
### Multiple SSE channels
|
|
|
|
```ts
|
|
const app = new Hype()
|
|
|
|
// Chat messages
|
|
app.sse('/api/chat', (send, c) => {
|
|
const room = c.req.query('room') ?? 'general'
|
|
// subscribe to chat room...
|
|
return () => { /* unsubscribe */ }
|
|
})
|
|
|
|
// Live notifications
|
|
app.sse('/api/notifications', (send, c) => {
|
|
send({ type: 'connected' })
|
|
const check = setInterval(async () => {
|
|
// check for new notifications...
|
|
send({ count: 5 }, 'notification')
|
|
}, 5000)
|
|
return () => clearInterval(check)
|
|
})
|
|
```
|
|
|
|
### Mixing SSR pages with a JSON API
|
|
|
|
```ts
|
|
import { Hype } from '@because/hype'
|
|
|
|
const app = new Hype({ pico: true })
|
|
|
|
// API routes (JSON)
|
|
app.get('/api/data', (c) => c.json({ items: [1, 2, 3] }))
|
|
|
|
// SSR pages still work via src/pages/
|
|
// Custom SSR route:
|
|
app.get('/dashboard', (c) => {
|
|
return c.html(
|
|
<html>
|
|
<body>
|
|
<h1>Dashboard</h1>
|
|
<div id="data"></div>
|
|
<script dangerouslySetInnerHTML={{ __html: `
|
|
fetch('/api/data')
|
|
.then(r => r.json())
|
|
.then(d => document.getElementById('data').textContent = JSON.stringify(d))
|
|
`}} />
|
|
</body>
|
|
</html>
|
|
)
|
|
})
|
|
|
|
export default app.defaults
|
|
```
|
|
|
|
### Using Hono middleware
|
|
|
|
Any Hono middleware works with Hype:
|
|
|
|
```ts
|
|
import { Hype } from '@because/hype'
|
|
import { cors } from 'hono/cors'
|
|
import { basicAuth } from 'hono/basic-auth'
|
|
|
|
const app = new Hype()
|
|
|
|
// CORS for API routes
|
|
app.use('/api/*', cors())
|
|
|
|
// Basic auth for admin
|
|
app.use('/admin/*', basicAuth({
|
|
username: 'admin',
|
|
password: process.env.ADMIN_PASSWORD!,
|
|
}))
|
|
|
|
export default app.defaults
|
|
```
|