diff --git a/README.md b/README.md index c089561..61ee391 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ - Page-based routing to `.tsx` files that export a `JSX` function in `./src/pages` - Transpile `.ts` files in `src/client/blah.ts` via `website.com/client/blah.ts` - Default, simple HTML5 layout with working frontend transpilation/bundling, or supply your own. +- Live reload in development mode — pages automatically refresh when the server restarts. - Optional CSS reset. - Optional pico.css. @@ -203,6 +204,30 @@ Test with curl: curl -N http://localhost:3000/api/time ``` +### Live Reload + +In development mode (`NODE_ENV !== 'production'`), `hype` automatically injects a live reload script into pages using the default layout. When the server restarts (e.g. via `bun --hot`), the browser will automatically reload the page. + +If you're using a custom layout, you can add live reload by importing and rendering the `ReloadScript` component: + +```tsx +import { ReloadScript } from "hype" + +export default ({ children, title }: any) => ( + + + {title ?? "hype"} + + +
{children}
+ + + +) +``` + +`ReloadScript` only renders in development mode — it's a no-op in production. + ### utils `hype` includes helpful utils for your webapp: diff --git a/examples/live-reload/package.json b/examples/live-reload/package.json new file mode 100644 index 0000000..ebbb398 --- /dev/null +++ b/examples/live-reload/package.json @@ -0,0 +1,15 @@ +{ + "name": "hype-live-reload-example", + "module": "src/index.tsx", + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "dependencies": { + "@because/hype": "*" + }, + "scripts": { + "start": "bun run src/server/index.ts", + "dev": "bun run --hot src/server/index.ts" + } +} diff --git a/examples/live-reload/src/client/main.ts b/examples/live-reload/src/client/main.ts new file mode 100644 index 0000000..bc485dc --- /dev/null +++ b/examples/live-reload/src/client/main.ts @@ -0,0 +1 @@ +console.log('Live reload is active in dev mode — no setup required.') diff --git a/examples/live-reload/src/css/main.css b/examples/live-reload/src/css/main.css new file mode 100644 index 0000000..7ba0262 --- /dev/null +++ b/examples/live-reload/src/css/main.css @@ -0,0 +1,29 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + display: flex; + justify-content: center; + padding-top: 4rem; +} + +section { + max-width: 500px; + text-align: center; +} + +code { + background: #f0f0f0; + padding: 0.2rem 0.5rem; + border-radius: 4px; + font-size: 0.9em; +} + +@media (prefers-color-scheme: dark) { + code { + background: #333; + } +} + +.hint { + color: #888; + font-style: italic; +} diff --git a/examples/live-reload/src/pages/index.tsx b/examples/live-reload/src/pages/index.tsx new file mode 100644 index 0000000..ed57a79 --- /dev/null +++ b/examples/live-reload/src/pages/index.tsx @@ -0,0 +1,8 @@ +export default () => ( +
+

Live Reload Demo

+

Run this with bun run dev, then edit this file.

+

The browser will automatically refresh — no manual reload needed.

+

Try changing this text and saving!

+
+) diff --git a/examples/live-reload/src/server/index.ts b/examples/live-reload/src/server/index.ts new file mode 100644 index 0000000..273680a --- /dev/null +++ b/examples/live-reload/src/server/index.ts @@ -0,0 +1,5 @@ +import { Hype } from '@because/hype' + +const app = new Hype({}) + +export default app.defaults diff --git a/src/index.tsx b/src/index.tsx index 4cbe8a5..882a01c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,6 +13,7 @@ const CSS_RESET = await Bun.file(join(import.meta.dir, '/css/reset.css')).text() const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text() export * from './utils' +export { ReloadScript } from './layout' export type { Context } from 'hono' const pageCache = new Map() @@ -142,6 +143,11 @@ export class Hype< if (this.routesRegistered) return this.routesRegistered = true + // live reload endpoint (dev mode only) + if (process.env.NODE_ENV !== 'production') { + this.sse('/__hype/reload', () => {}) + } + // healthcheck if (this.props.ok) this.get('/ok', c => c.text('ok')) @@ -246,3 +252,4 @@ function findAvailablePort(startPort: number, maxAttempts = 100): number { function render404(c: Context) { return c.text('File not found', 404) } + diff --git a/src/layout.tsx b/src/layout.tsx index 650eb92..263cb25 100644 --- a/src/layout.tsx +++ b/src/layout.tsx @@ -1,5 +1,7 @@ import { type FC } from 'hono/jsx' +const isDev = process.env.NODE_ENV !== 'production' + const Layout: FC = ({ children, title, props }) => @@ -17,7 +19,13 @@ const Layout: FC = ({ children, title, props }) =>
{children}
+ {isDev && } +const RELOAD_SCRIPT = '{let c=false;const e=new EventSource("/__hype/reload");e.onopen=()=>{if(c)location.reload();c=true};e.onerror=()=>{e.close();let d=50;setTimeout(function r(){fetch("/__hype/reload").then(()=>location.reload()).catch(()=>{d=Math.min(d*1.5,2000);setTimeout(r,d)})},d)}}' + +export const ReloadScript: FC = () => + isDev ?