diff --git a/docs/GUIDE.md b/docs/GUIDE.md new file mode 100644 index 0000000..bbaaa2c --- /dev/null +++ b/docs/GUIDE.md @@ -0,0 +1,1225 @@ +# 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, inline CSS/JS helpers, 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) +- [The `fe()` Helper](#the-fe-helper) +- [Inline `css` and `js` Tags](#inline-css-and-js-tags) +- [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 () => ( +
+

Hello, world!

+
+) +``` + +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 () => ( +
+

Welcome

+

This is server-rendered HTML.

+ About +
+) +``` + +```tsx +// src/pages/about.tsx +export default () => ( +
+

About

+

This website was made using futuristic internet technologies.

+ <= Back +
+) +``` + +The default layout automatically includes `src/css/main.css` and `src/client/main.ts`, wraps your page content in ``, ``, and `
...
`. + +### 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 }) => ( +
+

Hello, {req.query('name') ?? 'stranger'}!

+
+) +``` + +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 ( +
+

Request Info

+

URL: {req.url}

+

User-Agent: {ua}

+
+ ) +} +``` + +--- + +## 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 () => ( + + + My SPA + + + + + + +
+ + + + +
{children}
+ + + +) + +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 }) => ( +
+

Home

+
+) +``` + +### Static JSX export + +```tsx +// src/pages/about.tsx +export default ( +
+

About

+
+) +``` + +### 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 () => ( +
+

Users

+ +
+) +``` + +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 ( +
+

{user.name}

+
+ ) +} +``` + +### 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 + +``` + +### 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 + +``` + +### 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 + +``` + +### Combining options + +```ts +const app = new Hype({ pico: true, reset: true }) +``` + +### Inline CSS + +Use the `css` template tag for scoped inline styles in any page: + +```tsx +import { css } from '@because/hype' + +export default () => ( +
+ {css` + .hero { + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + padding: 4rem 2rem; + text-align: center; + } + `} +
+

Welcome

+
+
+) +``` + +This renders a `