Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e45bd1d0c | |||
| 78eef47f46 | |||
| 1333deb5e9 | |||
| 45139e519d | |||
| 268cb9482a | |||
| 1b908f8ba3 | |||
| 8785cfe43c | |||
| add241a405 | |||
| 4ff0727006 | |||
| f6da338b46 | |||
| 7e17ab5751 | |||
|
|
1b19e09e14 | ||
|
|
3744454076 | ||
| 1354e41375 | |||
| e8c5eb9d85 | |||
| 1576d92efc | |||
| 834ae8623a | |||
| 5ed8673274 | |||
| 65ee95ca0c | |||
| b9b5641b26 | |||
|
|
06705bab5c | ||
|
|
fec3e69ac0 | ||
|
|
83b03fbdcd | ||
| a7ad3a5c1d | |||
| 85fa79518c | |||
| c054ca1f82 |
89
README.md
89
README.md
|
|
@ -13,10 +13,10 @@
|
||||||
`hype` wraps `hono` with useful features for fast prototyping:
|
`hype` wraps `hono` with useful features for fast prototyping:
|
||||||
|
|
||||||
- HTTP logging to the console (disable with `NO_HTTP_LOG` env variable)
|
- 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/`
|
- Static files served from `pub/`
|
||||||
- Page-based routing to `.tsx` files that export a `JSX` function in `./src/pages`
|
- 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`
|
- 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.
|
- Default, simple HTML5 layout with working frontend transpilation/bundling, or supply your own.
|
||||||
- Optional CSS reset.
|
- Optional CSS reset.
|
||||||
- Optional pico.css.
|
- 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 `_`.
|
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
|
### Layout
|
||||||
|
|
||||||
`hype` will wrap everything in a simple default layout unless `./src/pages/_layout.tsx` exists,
|
`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" />
|
<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
|
### css reset
|
||||||
|
|
||||||
To use reset.css with the default layout, create `Hype` with `{ reset: true }`:
|
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"
|
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
|
### pub
|
||||||
|
|
||||||
Anything in `pub/` is served as-is. Simple stuff.
|
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:
|
`hype` includes helpful utils for your webapp:
|
||||||
|
|
||||||
- `capitalize(str: string): string` - Capitalizes a word
|
- `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
|
- `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
|
- `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
|
- `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
|
- `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`
|
- `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
1055
docs/GUIDE.md
Normal file
File diff suppressed because it is too large
Load Diff
2324
docs/HYPE+FORGE.md
Normal file
2324
docs/HYPE+FORGE.md
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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.
|
|
||||||
|
|
@ -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:../.."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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')!)
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
1
examples/spa/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry=https://npm.nose.space
|
||||||
|
|
@ -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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,7 +9,7 @@
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hype": "git+https://git.nose.space/defunkt/hype"
|
"@because/hype": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/server/index.ts",
|
"start": "bun run src/server/index.ts",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Hype } from 'hype'
|
import { Hype } from '@because/hype'
|
||||||
|
|
||||||
const app = new Hype({ layout: false })
|
const app = new Hype({ layout: false })
|
||||||
|
|
||||||
|
|
|
||||||
1
examples/ssr/.npmrc
Normal file
1
examples/ssr/.npmrc
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
registry=https://npm.nose.space
|
||||||
|
|
@ -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=="],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -9,10 +9,10 @@
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"hype": "git+https://git.nose.space/defunkt/hype"
|
"@because/hype": "*"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "bun run src/server/index.ts",
|
"start": "bun run src/server/index.ts",
|
||||||
"dev": "bun run --hot src/server/index.ts"
|
"dev": "bun run --hot src/server/index.ts"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,5 @@
|
||||||
import { fe } from 'hype'
|
|
||||||
|
|
||||||
const test = fe(() => {
|
|
||||||
alert('ding dong')
|
|
||||||
})
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<a href="#" onclick={test}>test</a>
|
<a href="#" onclick="alert('ding dong')">test</a>
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
import { js } from 'hype'
|
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<section>
|
<section>
|
||||||
<h1>SSE Demo</h1>
|
<h1>SSE Demo</h1>
|
||||||
|
|
@ -12,7 +10,8 @@ export default () => (
|
||||||
<a href="/">Back to Home</a>
|
<a href="/">Back to Home</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{js`
|
<script dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
const statusEl = document.getElementById('status')
|
const statusEl = document.getElementById('status')
|
||||||
const timeEl = document.getElementById('time')
|
const timeEl = document.getElementById('time')
|
||||||
const countEl = document.getElementById('count')
|
const countEl = document.getElementById('count')
|
||||||
|
|
@ -36,6 +35,6 @@ export default () => (
|
||||||
statusEl.textContent = 'Disconnected'
|
statusEl.textContent = 'Disconnected'
|
||||||
statusEl.style.color = 'red'
|
statusEl.style.color = 'red'
|
||||||
}
|
}
|
||||||
`}
|
`}} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Hype } from 'hype'
|
import { Hype } from '@because/hype'
|
||||||
|
|
||||||
const app = new Hype({})
|
const app = new Hype({})
|
||||||
|
|
||||||
|
|
|
||||||
12
package.json
12
package.json
|
|
@ -1,11 +1,21 @@
|
||||||
{
|
{
|
||||||
"name": "hype",
|
"name": "@because/hype",
|
||||||
|
"version": "0.0.6",
|
||||||
"module": "src/index.tsx",
|
"module": "src/index.tsx",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.tsx",
|
".": "./src/index.tsx",
|
||||||
"./utils": "./src/utils.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": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest"
|
"@types/bun": "latest"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
135
src/index.tsx
135
src/index.tsx
|
|
@ -1,4 +1,4 @@
|
||||||
import { join } from 'path'
|
import { join, resolve } from 'path'
|
||||||
import { render as formatHTML } from './lib/html-formatter'
|
import { render as formatHTML } from './lib/html-formatter'
|
||||||
import { type Context, Hono, type Schema, type Env } from 'hono'
|
import { type Context, Hono, type Schema, type Env } from 'hono'
|
||||||
import { serveStatic } from 'hono/bun'
|
import { serveStatic } from 'hono/bun'
|
||||||
|
|
@ -7,41 +7,27 @@ import color from 'kleur'
|
||||||
|
|
||||||
import { transpile } from './utils'
|
import { transpile } from './utils'
|
||||||
import defaultLayout from './layout'
|
import defaultLayout from './layout'
|
||||||
import { feFunctions, fnStorage } from './frontend'
|
|
||||||
|
|
||||||
const SHOW_HTTP_LOG = true
|
const SHOW_HTTP_LOG = true
|
||||||
const CSS_RESET = await Bun.file(join(import.meta.dir, '/css/reset.css')).text()
|
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()
|
const PICO_CSS = await Bun.file(join(import.meta.dir, '/css/pico.css')).text()
|
||||||
|
|
||||||
export * from './utils'
|
export * from './utils'
|
||||||
export { frontend, frontend as fe } from './frontend'
|
|
||||||
export type { Context } from 'hono'
|
export type { Context } from 'hono'
|
||||||
|
|
||||||
const pageCache = new Map()
|
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 = {
|
export type HypeProps = {
|
||||||
pico?: boolean
|
pico?: boolean
|
||||||
reset?: boolean
|
reset?: boolean
|
||||||
prettyHTML?: boolean
|
prettyHTML?: boolean
|
||||||
layout?: boolean
|
layout?: boolean
|
||||||
|
logging?: boolean
|
||||||
|
ok?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type InternalProps = HypeProps & { _isRouter?: boolean }
|
||||||
|
|
||||||
export type SSEHandler<E extends Env> = (
|
export type SSEHandler<E extends Env> = (
|
||||||
send: (data: unknown, event?: string) => Promise<void>,
|
send: (data: unknown, event?: string) => Promise<void>,
|
||||||
c: Context<E>
|
c: Context<E>
|
||||||
|
|
@ -53,12 +39,20 @@ export class Hype<
|
||||||
BasePath extends string = '/'
|
BasePath extends string = '/'
|
||||||
> extends Hono<E, S, BasePath> {
|
> extends Hono<E, S, BasePath> {
|
||||||
props: HypeProps
|
props: HypeProps
|
||||||
|
middlewareRegistered = false
|
||||||
routesRegistered = 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)
|
super(props)
|
||||||
|
|
||||||
this.props = 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>) {
|
sse(path: string, handler: SSEHandler<E>) {
|
||||||
|
|
@ -84,9 +78,9 @@ export class Hype<
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerRoutes() {
|
registerMiddleware() {
|
||||||
if (this.routesRegistered) return
|
if (this.middlewareRegistered) return
|
||||||
this.routesRegistered = true
|
this.middlewareRegistered = true
|
||||||
|
|
||||||
// static assets in pub/
|
// static assets in pub/
|
||||||
this.use('/*', serveStatic({ root: './pub' }))
|
this.use('/*', serveStatic({ root: './pub' }))
|
||||||
|
|
@ -95,16 +89,18 @@ export class Hype<
|
||||||
this.use('/css/*', serveStatic({ root: './src' }))
|
this.use('/css/*', serveStatic({ root: './src' }))
|
||||||
|
|
||||||
// console logging
|
// console logging
|
||||||
this.use('*', async (c, next) => {
|
if (this.props.logging !== false) {
|
||||||
if (!SHOW_HTTP_LOG) return await next()
|
this.use('*', async (c, next) => {
|
||||||
|
if (!SHOW_HTTP_LOG) return await next()
|
||||||
|
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
await next()
|
await next()
|
||||||
const end = Date.now()
|
const end = Date.now()
|
||||||
const fn = c.res.status < 400 ? color.green : c.res.status < 500 ? color.yellow : color.red
|
const fn = c.res.status < 400 ? color.green : c.res.status < 500 ? color.yellow : color.red
|
||||||
const method = c.req.method === 'GET' ? color.cyan(c.req.method) : color.magenta(c.req.method)
|
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)`)
|
console.log(fn(`${c.res.status}`), `${color.bold(method)} ${c.req.url} (${end - start}ms)`)
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// exception handler
|
// exception handler
|
||||||
this.onError((err, c) => {
|
this.onError((err, c) => {
|
||||||
|
|
@ -133,28 +129,20 @@ export class Hype<
|
||||||
const res = c.res.clone()
|
const res = c.res.clone()
|
||||||
const html = await res.text()
|
const html = await res.text()
|
||||||
const formatted = formatHTML(html)
|
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
|
registerRoutes() {
|
||||||
this.use('*', async (c, next) => {
|
if (this.routesRegistered) return
|
||||||
await fnStorage.run({ fns: new Map(), counter: 0 }, async () => {
|
this.routesRegistered = true
|
||||||
await next()
|
|
||||||
|
|
||||||
const contentType = c.res.headers.get('content-type')
|
// healthcheck
|
||||||
if (!contentType?.includes('text/html')) return
|
if (this.props.ok)
|
||||||
|
this.get('/ok', c => c.text('ok'))
|
||||||
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)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// css reset
|
// css reset
|
||||||
this.get('/css/reset.css', async c => new Response(CSS_RESET, { headers: { 'Content-Type': 'text/css' } }))
|
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
|
// serve transpiled js
|
||||||
this.on('GET', ['/client/:path{.+}', '/shared/:path{.+}'], async c => {
|
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
|
// strip known extension to get base path
|
||||||
if (!path.endsWith('.js') && !path.endsWith('.ts')) path += '.ts'
|
const base = reqPath.replace(/\.(js|jsx|ts|tsx)$/, '')
|
||||||
|
|
||||||
const ts = path.replace('.js', '.ts')
|
// try TS extensions first (needs transpilation)
|
||||||
if (await Bun.file(ts).exists())
|
for (const ext of ['.ts', '.tsx', '.jsx']) {
|
||||||
return new Response(await transpile(ts), { headers: { 'Content-Type': 'text/javascript' } })
|
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())
|
// try plain .js (serve raw)
|
||||||
return new Response(await transpile(ts + 'x'), { headers: { 'Content-Type': 'text/javascript' } })
|
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 render404(c)
|
||||||
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
|
// file based routing
|
||||||
this.on('GET', ['/', '/:page'], async c => {
|
this.on('GET', ['/', '/:page'], async c => {
|
||||||
const pageName = (c.req.param('page') ?? 'index').replace('.', '')
|
const pageName = (c.req.param('page') ?? 'index').replace('.', '')
|
||||||
if (pageName.startsWith('_')) return render404(c)
|
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()))
|
if (!(await Bun.file(path).exists()))
|
||||||
return render404(c)
|
return render404(c)
|
||||||
|
|
||||||
let Layout = defaultLayout
|
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()) {
|
if (await Bun.file(layoutPath).exists()) {
|
||||||
let Layout = pageCache.get(layoutPath)
|
Layout = pageCache.get(layoutPath)
|
||||||
if (!Layout) {
|
if (!Layout) {
|
||||||
Layout = (await import(layoutPath + `?t=${Date.now()}`)).default
|
Layout = (await import(layoutPath + `?t=${Date.now()}`)).default
|
||||||
pageCache.set(layoutPath, Layout)
|
pageCache.set(layoutPath, Layout)
|
||||||
|
|
@ -242,6 +223,8 @@ export class Hype<
|
||||||
|
|
||||||
// find an available port starting from the given port
|
// find an available port starting from the given port
|
||||||
function findAvailablePort(startPort: number, maxAttempts = 100): number {
|
function findAvailablePort(startPort: number, maxAttempts = 100): number {
|
||||||
|
if (process.env.NO_AUTOPORT) return startPort
|
||||||
|
|
||||||
for (let port = startPort; port < startPort + maxAttempts; port++) {
|
for (let port = startPort; port < startPort + maxAttempts; port++) {
|
||||||
try {
|
try {
|
||||||
const server = Bun.serve({ port, fetch: () => new Response() })
|
const server = Bun.serve({ port, fetch: () => new Response() })
|
||||||
|
|
@ -256,4 +239,4 @@ function findAvailablePort(startPort: number, maxAttempts = 100): number {
|
||||||
|
|
||||||
function render404(c: Context) {
|
function render404(c: Context) {
|
||||||
return c.text('File not found', 404)
|
return c.text('File not found', 404)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -46,8 +46,6 @@ const minify = function(el) {
|
||||||
.replace(/>\s+</g, '><')
|
.replace(/>\s+</g, '><')
|
||||||
.replace(/\s+/g, ' ')
|
.replace(/\s+/g, ' ')
|
||||||
.replace(/\s>/g, '>')
|
.replace(/\s>/g, '>')
|
||||||
.replace(/>\s/g, '>')
|
|
||||||
.replace(/\s</g, '<')
|
|
||||||
.replace(/class=["']\s/g, function(match) {
|
.replace(/class=["']\s/g, function(match) {
|
||||||
return match.replace(/\s/g, '');
|
return match.replace(/\s/g, '');
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,6 @@
|
||||||
import { type Context } from 'hono'
|
import { type Context } from 'hono'
|
||||||
import { stat } from 'fs/promises'
|
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
|
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
||||||
export function lightenColor(hex: string, opacity: number): string {
|
export function lightenColor(hex: string, opacity: number): string {
|
||||||
// Remove # if present
|
// Remove # if present
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user