Compare commits

..

26 Commits

Author SHA1 Message Date
8e45bd1d0c 6 2026-02-20 16:25:47 -08:00
78eef47f46 Remove css, js, and fe() inline helpers; update docs and examples to use client scripts 2026-02-20 16:18:24 -08:00
1333deb5e9 Merge branch 'fix-ts-import' 2026-02-20 16:10:32 -08:00
45139e519d 5 2026-02-20 16:08:43 -08:00
268cb9482a Fix path traversal vulnerability in static file serving 2026-02-20 16:08:25 -08:00
1b908f8ba3 Refactor client file serving to support .jsx extension and clean up extension resolution logic 2026-02-20 16:05:01 -08:00
8785cfe43c some guides/docs 2026-02-19 10:07:42 -08:00
add241a405 4 2026-02-18 18:48:58 -08:00
4ff0727006 use proper cwd 2026-02-18 18:48:52 -08:00
f6da338b46 0.0.3 2026-02-16 13:26:32 -08:00
7e17ab5751 ok 2026-02-16 13:26:21 -08:00
Chris Wanstrath
1b19e09e14 prettyHTML too aggressive in eating up extra spaces 2026-02-09 16:23:51 -08:00
Chris Wanstrath
3744454076 fix variable shadow bug 2026-02-09 11:57:19 -08:00
1354e41375 update hype examples to use @because/hype 2026-02-09 08:57:21 -08:00
e8c5eb9d85 0.0.2 2026-01-30 21:55:02 -08:00
1576d92efc logging: false 2026-01-30 21:54:44 -08:00
834ae8623a fix header lengths 2026-01-30 20:33:39 -08:00
5ed8673274 @because/hype 2026-01-29 19:38:02 -08:00
65ee95ca0c v0.1.3 2026-01-29 19:34:55 -08:00
b9b5641b26 fix router 2026-01-29 18:41:49 -08:00
Chris Wanstrath
06705bab5c v0.1.1 2026-01-29 18:34:40 -08:00
Chris Wanstrath
fec3e69ac0 npm 2026-01-29 18:34:04 -08:00
Chris Wanstrath
83b03fbdcd version 2026-01-29 18:34:04 -08:00
a7ad3a5c1d router: true 2026-01-29 18:31:32 -08:00
85fa79518c register middleware early 2026-01-28 10:56:30 -08:00
c054ca1f82 NO_AUTOPORT 2026-01-28 10:52:00 -08:00
24 changed files with 3463 additions and 430 deletions

1
.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://npm.nose.space

View File

@ -13,10 +13,10 @@
`hype` wraps `hono` with useful features for fast prototyping:
- HTTP logging to the console (disable with `NO_HTTP_LOG` env variable)
- Auto-port selection in dev mode: if the port is busy, tries the next one (disable with `NO_AUTOPORT` env variable)
- Static files served from `pub/`
- 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`
- Helpers like `css` and `js` template tags.
- Default, simple HTML5 layout with working frontend transpilation/bundling, or supply your own.
- Optional CSS reset.
- Optional pico.css.
@ -60,51 +60,6 @@ The `req` JSX prop will be given the `Hono` request:
To put a file in `./src/pages` but prevent it from being server, preface it with `_`.
### SPA Shell Mode
If `src/client/index.tsx` (or `src/client/index.ts`) exists, hype automatically serves a minimal HTML shell at `/` - no `src/pages/index.tsx` needed.
```
src/
client/
index.tsx <- This triggers SPA shell mode!
server/
index.ts
```
The shell HTML served at `/`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/client/index.js"></script>
</body>
</html>
```
Your client code mounts to `#app`:
```tsx
// src/client/index.tsx
import { render } from 'hono/jsx/dom'
function App() {
return <h1>Hello from the SPA!</h1>
}
render(<App />, document.getElementById('app')!)
```
See `examples/spa-shell` for a complete example.
### Layout
`hype` will wrap everything in a simple default layout unless `./src/pages/_layout.tsx` exists,
@ -159,25 +114,6 @@ CSS can be accessed via `/css/main.css`:
<link href="/css/main.css" rel="stylesheet" />
```
Or written inline using the `css` template tag:
```tsx
import { css } from "hype"
export default () => (
<div>
{css`
* {
color: red;
}
`}
<h1>Hello</h1>
</div>
)
```
Install the `vscode-styled-components` VSCode extension to highlight inline `css` tags!
### css reset
To use reset.css with the default layout, create `Hype` with `{ reset: true }`:
@ -209,27 +145,6 @@ import { initAbout } from "./about"
import utils from "./shared/utils"
```
Or written inline as transpiled typescript using the `js` template tag:
```tsx
import { js } from "hype"
export default () => (
<div>
{js`
window.onload = () => alert(welcomeMsg(Date.now()))
function welcomeMsg(time: number): string {
return "Welcome to my website!"
}
`}
<h1>Hello!</h1>
</div>
)
```
Install the `vscode-styled-components` VSCode extension to highlight inline `js` tags!
### pub
Anything in `pub/` is served as-is. Simple stuff.
@ -293,10 +208,8 @@ curl -N http://localhost:3000/api/time
`hype` includes helpful utils for your webapp:
- `capitalize(str: string): string` - Capitalizes a word
- `css` Template Tag - Lets you inline CSS in your TSX. Returns a `<style>` tag
- `darkenColor(hex: string, opacity: number): string` - Darken a hex color by blending with black. opacity 1 = original, 0 = black
- `isDarkMode(): boolean` - Check if the user prefers dark mode
- `js` Template Tag - Lets you inline JavaScript in your TSX. Transpiles and returns a `<script>` tag
- `lightenColor(hex: string, opacity: number): string` - Lighten a hex color by blending with white. opacity 1 = original, 0 = white
- `rand(end = 2, startAtZero = false): number` - Generate random integer. `rand(2)` flips a coin, `rand(6)` rolls a die, `rand(20)` rolls d20
- `randIndex<T>(list: T[]): number | undefined` - Get a random index from an array. `randIndex([5, 7, 9])` returns `0`, `1`, or `2`

1055
docs/GUIDE.md Normal file

File diff suppressed because it is too large Load Diff

2324
docs/HYPE+FORGE.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,44 +0,0 @@
# SPA Shell Mode Example
This example demonstrates the SPA shell mode feature. When `src/client/index.tsx` (or `.ts`) exists, hype automatically serves a minimal HTML shell at `/` - no `src/pages/index.tsx` needed.
## Structure
```
src/
client/
index.tsx <- This triggers SPA shell mode!
server/
index.ts
```
## Run
```sh
bun install
bun dev
```
Visit http://localhost:3000 to see the SPA in action.
## How it works
When hype detects `src/client/index.tsx` or `src/client/index.ts`, it serves this HTML at `/`:
```html
<!DOCTYPE html>
<html lang="en">
<head>
<title>App</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
</head>
<body>
<div id="app"></div>
<script type="module" src="/client/index.js"></script>
</body>
</html>
```
Your client code mounts to `#app` and takes over from there.

View File

@ -1,10 +0,0 @@
{
"name": "spa-shell-example",
"scripts": {
"start": "bun run src/server/index.ts",
"dev": "bun run --hot src/server/index.ts"
},
"dependencies": {
"hype": "link:../.."
}
}

View File

@ -1,21 +0,0 @@
import { render } from 'hono/jsx/dom'
import { useState } from 'hono/jsx/dom'
function App() {
const [count, setCount] = useState(0)
return (
<>
<h1>SPA Shell Mode</h1>
<p>This page is served from a minimal HTML shell - no <code>src/pages/index.tsx</code> needed!</p>
<h2>Count: {count}</h2>
<div>
<button onClick={() => setCount(c => c + 1)}>+</button>
{' '}
<button onClick={() => setCount(c => c && c - 1)}>-</button>
</div>
</>
)
}
render(<App />, document.getElementById('app')!)

View File

@ -1,8 +0,0 @@
import { Hype } from 'hype'
const app = new Hype()
// Add your API routes here
app.get('/api/hello', (c) => c.json({ message: 'Hello from the API!' }))
export default app.defaults

View File

@ -1,29 +0,0 @@
{
"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,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false,
"baseUrl": ".",
"paths": {
"$*": ["src/server/*"],
"#*": ["src/client/*"],
"@*": ["src/shared/*"]
}
}
}

1
examples/spa/.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://npm.nose.space

View File

@ -1,35 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "hype-spa-example",
"dependencies": {
"hype": "git+https://git.nose.space/defunkt/hype",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#33d228c9ac6a01fad570e0ac2ba836a100dde623", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "33d228c9ac6a01fad570e0ac2ba836a100dde623"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

View File

@ -9,7 +9,7 @@
"typescript": "^5"
},
"dependencies": {
"hype": "git+https://git.nose.space/defunkt/hype"
"@because/hype": "*"
},
"scripts": {
"start": "bun run src/server/index.ts",

View File

@ -1,4 +1,4 @@
import { Hype } from 'hype'
import { Hype } from '@because/hype'
const app = new Hype({ layout: false })

1
examples/ssr/.npmrc Normal file
View File

@ -0,0 +1 @@
registry=https://npm.nose.space

View File

@ -1,35 +0,0 @@
{
"lockfileVersion": 1,
"configVersion": 1,
"workspaces": {
"": {
"name": "hype-ssr-example",
"dependencies": {
"hype": "git+https://git.nose.space/defunkt/hype",
},
"devDependencies": {
"@types/bun": "latest",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/node": ["@types/node@25.0.9", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-/rpCXHlCWeqClNBwUhDcusJxXYDjZTyE8v5oTO7WbL8eij2nKhUeU89/6xgjU7N4/Vh3He0BtyhJdQbDyhiXAw=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"hono": ["hono@4.11.4", "", {}, "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA=="],
"hype": ["hype@git+https://git.nose.space/defunkt/hype#33d228c9ac6a01fad570e0ac2ba836a100dde623", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "33d228c9ac6a01fad570e0ac2ba836a100dde623"],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
}
}

View File

@ -9,7 +9,7 @@
"typescript": "^5"
},
"dependencies": {
"hype": "git+https://git.nose.space/defunkt/hype"
"@because/hype": "*"
},
"scripts": {
"start": "bun run src/server/index.ts",

View File

@ -1,11 +1,5 @@
import { fe } from 'hype'
const test = fe(() => {
alert('ding dong')
})
export default () => (
<section>
<a href="#" onclick={test}>test</a>
<a href="#" onclick="alert('ding dong')">test</a>
</section>
)

View File

@ -1,5 +1,3 @@
import { js } from 'hype'
export default () => (
<section>
<h1>SSE Demo</h1>
@ -12,7 +10,8 @@ export default () => (
<a href="/">Back to Home</a>
</p>
{js`
<script dangerouslySetInnerHTML={{
__html: `
const statusEl = document.getElementById('status')
const timeEl = document.getElementById('time')
const countEl = document.getElementById('count')
@ -36,6 +35,6 @@ export default () => (
statusEl.textContent = 'Disconnected'
statusEl.style.color = 'red'
}
`}
`}} />
</section>
)

View File

@ -1,4 +1,4 @@
import { Hype } from 'hype'
import { Hype } from '@because/hype'
const app = new Hype({})

View File

@ -1,11 +1,21 @@
{
"name": "hype",
"name": "@because/hype",
"version": "0.0.6",
"module": "src/index.tsx",
"type": "module",
"exports": {
".": "./src/index.tsx",
"./utils": "./src/utils.tsx"
},
"files": [
"src/css",
"src/frontend.ts",
"src/index.tsx",
"src/layout.tsx",
"src/lib",
"src/utils.tsx",
"README.md"
],
"devDependencies": {
"@types/bun": "latest"
},

View File

@ -1,48 +0,0 @@
import { AsyncLocalStorage } from 'async_hooks'
export const fnStorage = new AsyncLocalStorage<{
fns: Map<string, string>
counter: number
}>()
export function fe<T extends Record<string, unknown>>(
fn: (args?: T) => void,
args?: T
): string {
const store = fnStorage.getStore()
if (!store) {
// Fallback to IIFE if outside request context
return args
? `(${fn.toString()})(${JSON.stringify(args)})`
: `(${fn.toString()})()`
}
const fnStr = fn.toString()
// Dedupe by function body
for (const [name, body] of store.fns)
if (body === fnStr)
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
const name = `frontendFn${store.counter++}`
store.fns.set(name, fnStr)
return args ? `${name}(${JSON.stringify(args)})` : `${name}()`
}
export function feFunctions(): string[] {
const store = fnStorage.getStore()
if (!store?.fns.size) return []
return [...store.fns.entries()].map(([name, body]) => {
// Handle arrow functions vs regular functions
if (body.startsWith('(') || body.startsWith('async ('))
return `const ${name} = ${body};`
// Named function - rename it
return body.replace(/^(async\s+)?function\s*\w*/, `$1function ${name}`)
})
}
// Keep for backwards compat
export const frontend = fe

View File

@ -1,4 +1,4 @@
import { join } from 'path'
import { join, resolve } from 'path'
import { render as formatHTML } from './lib/html-formatter'
import { type Context, Hono, type Schema, type Env } from 'hono'
import { serveStatic } from 'hono/bun'
@ -7,41 +7,27 @@ import color from 'kleur'
import { transpile } from './utils'
import defaultLayout from './layout'
import { feFunctions, fnStorage } from './frontend'
const SHOW_HTTP_LOG = true
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 { frontend, frontend as fe } from './frontend'
export type { Context } from 'hono'
const pageCache = new Map()
function generateShellHtml(title = 'App', mountId = 'app', clientEntry = '/client/index.js'): string {
return `<!DOCTYPE html>
<html lang="en">
<head>
<title>${title}</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
</head>
<body>
<div id="${mountId}"></div>
<script type="module" src="${clientEntry}"></script>
</body>
</html>`
}
export type HypeProps = {
pico?: boolean
reset?: boolean
prettyHTML?: boolean
layout?: boolean
logging?: boolean
ok?: boolean
}
type InternalProps = HypeProps & { _isRouter?: boolean }
export type SSEHandler<E extends Env> = (
send: (data: unknown, event?: string) => Promise<void>,
c: Context<E>
@ -53,12 +39,20 @@ export class Hype<
BasePath extends string = '/'
> extends Hono<E, S, BasePath> {
props: HypeProps
middlewareRegistered = false
routesRegistered = false
constructor(props?: HypeProps & ConstructorParameters<typeof Hono<E, S, BasePath>>[0]) {
constructor(props?: InternalProps & ConstructorParameters<typeof Hono<E, S, BasePath>>[0]) {
super(props)
this.props = props ?? {}
if (!props?._isRouter) {
this.registerMiddleware()
}
}
static router<E extends Env = Env>(): Hype<E> {
return new Hype<E>({ _isRouter: true })
}
sse(path: string, handler: SSEHandler<E>) {
@ -84,9 +78,9 @@ export class Hype<
})
}
async registerRoutes() {
if (this.routesRegistered) return
this.routesRegistered = true
registerMiddleware() {
if (this.middlewareRegistered) return
this.middlewareRegistered = true
// static assets in pub/
this.use('/*', serveStatic({ root: './pub' }))
@ -95,6 +89,7 @@ export class Hype<
this.use('/css/*', serveStatic({ root: './src' }))
// console logging
if (this.props.logging !== false) {
this.use('*', async (c, next) => {
if (!SHOW_HTTP_LOG) return await next()
@ -105,6 +100,7 @@ export class Hype<
const method = c.req.method === 'GET' ? color.cyan(c.req.method) : color.magenta(c.req.method)
console.log(fn(`${c.res.status}`), `${color.bold(method)} ${c.req.url} (${end - start}ms)`)
})
}
// exception handler
this.onError((err, c) => {
@ -133,28 +129,20 @@ export class Hype<
const res = c.res.clone()
const html = await res.text()
const formatted = formatHTML(html)
c.res = new Response(formatted, c.res)
const headers = new Headers(c.res.headers)
headers.delete('content-length')
c.res = new Response(formatted, { status: c.res.status, headers })
})
}
}
// serve frontend js
this.use('*', async (c, next) => {
await fnStorage.run({ fns: new Map(), counter: 0 }, async () => {
await next()
registerRoutes() {
if (this.routesRegistered) return
this.routesRegistered = true
const contentType = c.res.headers.get('content-type')
if (!contentType?.includes('text/html')) return
const fns = feFunctions()
if (!fns.length) return
const res = c.res.clone()
const html = await res.text()
const newHtml = html.replace('</body>', `<script>${fns.join('\n')}</script></body>`)
c.res = new Response(newHtml, c.res)
})
})
// healthcheck
if (this.props.ok)
this.get('/ok', c => c.text('ok'))
// css reset
this.get('/css/reset.css', async c => new Response(CSS_RESET, { headers: { 'Content-Type': 'text/css' } }))
@ -164,48 +152,41 @@ export class Hype<
// serve transpiled js
this.on('GET', ['/client/:path{.+}', '/shared/:path{.+}'], async c => {
let path = './src/' + c.req.path.replace('..', '.')
const reqPath = resolve('./src/', c.req.path.slice(1))
if (!reqPath.startsWith(resolve('./src/'))) return render404(c)
// path must end in .js or .ts
if (!path.endsWith('.js') && !path.endsWith('.ts')) path += '.ts'
// strip known extension to get base path
const base = reqPath.replace(/\.(js|jsx|ts|tsx)$/, '')
const ts = path.replace('.js', '.ts')
if (await Bun.file(ts).exists())
return new Response(await transpile(ts), { headers: { 'Content-Type': 'text/javascript' } })
// try TS extensions first (needs transpilation)
for (const ext of ['.ts', '.tsx', '.jsx']) {
const file = base + ext
if (await Bun.file(file).exists())
return new Response(await transpile(file), { headers: { 'Content-Type': 'text/javascript' } })
}
else if (await Bun.file(ts + 'x').exists())
return new Response(await transpile(ts + 'x'), { headers: { 'Content-Type': 'text/javascript' } })
// try plain .js (serve raw)
const jsFile = base + '.js'
if (await Bun.file(jsFile).exists())
return new Response(Bun.file(jsFile), { headers: { 'Content-Type': 'text/javascript' } })
else if (await Bun.file(path).exists())
return new Response(Bun.file(path), { headers: { 'Content-Type': 'text/javascript' } })
else
return render404(c)
})
// SPA auto-detection: serve shell if src/client/index.tsx exists
const clientIndexPath = join(process.env.PWD ?? '.', 'src/client/index.tsx')
const clientIndexTsPath = join(process.env.PWD ?? '.', 'src/client/index.ts')
const hasSpaClient = await Bun.file(clientIndexPath).exists() || await Bun.file(clientIndexTsPath).exists()
if (hasSpaClient) {
this.get('/', (c) => c.html(generateShellHtml()))
}
// file based routing
this.on('GET', ['/', '/:page'], async c => {
const pageName = (c.req.param('page') ?? 'index').replace('.', '')
if (pageName.startsWith('_')) return render404(c)
const path = join(process.env.PWD ?? '.', `./src/pages/${pageName}.tsx`)
const path = join(process.cwd(), `./src/pages/${pageName}.tsx`)
if (!(await Bun.file(path).exists()))
return render404(c)
let Layout = defaultLayout
const layoutPath = join(process.env.PWD ?? '.', `./src/pages/_layout.tsx`)
const layoutPath = join(process.cwd(), `./src/pages/_layout.tsx`)
if (await Bun.file(layoutPath).exists()) {
let Layout = pageCache.get(layoutPath)
Layout = pageCache.get(layoutPath)
if (!Layout) {
Layout = (await import(layoutPath + `?t=${Date.now()}`)).default
pageCache.set(layoutPath, Layout)
@ -242,6 +223,8 @@ export class Hype<
// find an available port starting from the given port
function findAvailablePort(startPort: number, maxAttempts = 100): number {
if (process.env.NO_AUTOPORT) return startPort
for (let port = startPort; port < startPort + maxAttempts; port++) {
try {
const server = Bun.serve({ port, fetch: () => new Response() })

View File

@ -46,8 +46,6 @@ const minify = function(el) {
.replace(/>\s+</g, '><')
.replace(/\s+/g, ' ')
.replace(/\s>/g, '>')
.replace(/>\s/g, '>')
.replace(/\s</g, '<')
.replace(/class=["']\s/g, function(match) {
return match.replace(/\s/g, '');
})

View File

@ -1,22 +1,6 @@
import { type Context } from 'hono'
import { stat } from 'fs/promises'
// template literal tag for inline CSS. returns a <style> tag
export function css(strings: TemplateStringsArray, ...values: any[]) {
return <style dangerouslySetInnerHTML={{
__html: String.raw({ raw: strings }, ...values)
}} />
}
const transpiler = new Bun.Transpiler({ loader: 'tsx' })
// template literal tag for inline JS. transpiles and returns a <script> tag
export function js(strings: TemplateStringsArray, ...values: any[]) {
return <script dangerouslySetInnerHTML={{
__html: transpiler.transformSync(String.raw({ raw: strings }, ...values))
}} />
}
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
export function lightenColor(hex: string, opacity: number): string {
// Remove # if present