hype
This commit is contained in:
commit
1f8c6bde50
34
.gitignore
vendored
Normal file
34
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# dependencies (bun install)
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# output
|
||||||
|
out
|
||||||
|
dist
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# code coverage
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# logs
|
||||||
|
logs
|
||||||
|
_.log
|
||||||
|
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# caches
|
||||||
|
.eslintcache
|
||||||
|
.cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Finder (MacOS) folder config
|
||||||
|
.DS_Store
|
||||||
111
CLAUDE.md
Normal file
111
CLAUDE.md
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
---
|
||||||
|
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||||
|
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||||
|
alwaysApply: false
|
||||||
|
---
|
||||||
|
|
||||||
|
Default to using Bun instead of Node.js.
|
||||||
|
|
||||||
|
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||||
|
- Use `bun test` instead of `jest` or `vitest`
|
||||||
|
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||||
|
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||||
|
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||||
|
- Bun automatically loads .env, so don't use dotenv.
|
||||||
|
|
||||||
|
## APIs
|
||||||
|
|
||||||
|
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||||
|
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||||
|
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||||
|
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||||
|
- `WebSocket` is built-in. Don't use `ws`.
|
||||||
|
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||||
|
- Bun.$`ls` instead of execa.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Use `bun test` to run tests.
|
||||||
|
|
||||||
|
```ts#index.test.ts
|
||||||
|
import { test, expect } from "bun:test";
|
||||||
|
|
||||||
|
test("hello world", () => {
|
||||||
|
expect(1).toBe(1);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||||
|
|
||||||
|
Server:
|
||||||
|
|
||||||
|
```ts#index.ts
|
||||||
|
import index from "./index.html"
|
||||||
|
|
||||||
|
Bun.serve({
|
||||||
|
routes: {
|
||||||
|
"/": index,
|
||||||
|
"/api/users/:id": {
|
||||||
|
GET: (req) => {
|
||||||
|
return new Response(JSON.stringify({ id: req.params.id }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// optional websocket support
|
||||||
|
websocket: {
|
||||||
|
open: (ws) => {
|
||||||
|
ws.send("Hello, world!");
|
||||||
|
},
|
||||||
|
message: (ws, message) => {
|
||||||
|
ws.send(message);
|
||||||
|
},
|
||||||
|
close: (ws) => {
|
||||||
|
// handle close
|
||||||
|
}
|
||||||
|
},
|
||||||
|
development: {
|
||||||
|
hmr: true,
|
||||||
|
console: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||||
|
|
||||||
|
```html#index.html
|
||||||
|
<html>
|
||||||
|
<body>
|
||||||
|
<h1>Hello, world!</h1>
|
||||||
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
With the following `frontend.tsx`:
|
||||||
|
|
||||||
|
```tsx#frontend.tsx
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
// import .css files directly and it works
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
|
||||||
|
const root = createRoot(document.body);
|
||||||
|
|
||||||
|
export default function Frontend() {
|
||||||
|
return <h1>Hello, world!</h1>;
|
||||||
|
}
|
||||||
|
|
||||||
|
root.render(<Frontend />);
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run index.ts
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun --hot ./index.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||||
156
README.md
Normal file
156
README.md
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
# hype
|
||||||
|
|
||||||
|
░▒▓███████████████████████████████████████████████▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓███████▓▒░░▒▓████████▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░░▒▓█▓▒░▒▓█▓▒░
|
||||||
|
░▒▓████████▓▒░░▒▓██████▓▒░░▒▓███████▓▒░░▒▓██████▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░
|
||||||
|
░▒▓█▓▒░░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓█▓▒░ ░▒▓████████▓▒░
|
||||||
|
░▒▓███████████████████████████████████████████████▓▒░
|
||||||
|
|
||||||
|
`hype` wraps `hono` with useful defaults:
|
||||||
|
|
||||||
|
- HTTP logging (disable with `NO_HTTP_LOG` 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/js/blah.ts` via `website.com/js/blah.ts`
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Your project structure should be:
|
||||||
|
|
||||||
|
```
|
||||||
|
.
|
||||||
|
├── README.md
|
||||||
|
├── package.json
|
||||||
|
├── pub
|
||||||
|
│ ├── asset.txt
|
||||||
|
│ └── img
|
||||||
|
│ └── logo.png
|
||||||
|
├── src
|
||||||
|
│ ├── css
|
||||||
|
│ │ └── main.css
|
||||||
|
│ ├── js
|
||||||
|
│ │ ├── about.ts
|
||||||
|
│ │ └── main.ts
|
||||||
|
│ ├── shared
|
||||||
|
│ │ └── utils.ts
|
||||||
|
│ ├── pages
|
||||||
|
│ │ ├── _layout.tsx
|
||||||
|
│ │ ├── about.tsx
|
||||||
|
│ │ └── index.tsx
|
||||||
|
│ └── server.tsx
|
||||||
|
└── tsconfig.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### URLs
|
||||||
|
|
||||||
|
In the above, `/about` will render `./src/pages/about.tsx`.
|
||||||
|
|
||||||
|
The `req` JSX prop will be given the `Hono` request:
|
||||||
|
|
||||||
|
export default ({ req }) => `Query Param 'id': ${req.query('id')}`
|
||||||
|
|
||||||
|
If `_layout.tsx` exists, your pages will automatically be wrapped in it:
|
||||||
|
|
||||||
|
export default ({ children, title }: any) =>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{title ?? 'hype'}</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="/js/main.ts" type="module"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
|
(Files starting with `_` don't get served directly.)
|
||||||
|
|
||||||
|
### css
|
||||||
|
|
||||||
|
CSS can be accessed via `/css/main.css`:
|
||||||
|
|
||||||
|
<link href="/css/reset.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
### js
|
||||||
|
|
||||||
|
JS can be accessed (and transpiled) via `/js/main.ts` or `/shared/utils.ts`:
|
||||||
|
|
||||||
|
<script src="/js/main.ts" type="module"></script>
|
||||||
|
|
||||||
|
import your modules relatively, for example in `main.ts`:
|
||||||
|
|
||||||
|
import { initAbout } from './about'
|
||||||
|
import utils from './shared/utils'
|
||||||
|
|
||||||
|
### pub
|
||||||
|
|
||||||
|
Anything in `pub/` is served as-is. Simple stuff.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Add `dev` and `start` to `package.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"scripts": {
|
||||||
|
"start": "bun run src/server.tsx",
|
||||||
|
"dev": "bun run --hot src/server.tsx"
|
||||||
|
},
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Use our `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,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"#*": ["src/*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use `Hype` just like `Hono` in your `server.tsx`, exporting `app.defaults`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Hype } from "hype"
|
||||||
|
|
||||||
|
const app = new Hype()
|
||||||
|
|
||||||
|
app.get("/my-custom-routes", (c) => c.text("wild, wild stuff"))
|
||||||
|
|
||||||
|
export default app.defaults
|
||||||
|
```
|
||||||
|
|
||||||
|
4. Enter dev mode
|
||||||
|
|
||||||
|
bun install
|
||||||
|
bun test
|
||||||
37
bun.lock
Normal file
37
bun.lock
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
{
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"workspaces": {
|
||||||
|
"": {
|
||||||
|
"name": "hype",
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.10.4",
|
||||||
|
"kleur": "^4.1.5",
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest",
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"packages": {
|
||||||
|
"@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="],
|
||||||
|
|
||||||
|
"@types/node": ["@types/node@24.10.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A=="],
|
||||||
|
|
||||||
|
"@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="],
|
||||||
|
|
||||||
|
"bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="],
|
||||||
|
|
||||||
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"hono": ["hono@4.10.4", "", {}, "sha512-YG/fo7zlU3KwrBL5vDpWKisLYiM+nVstBQqfr7gCPbSYURnNEP9BDxEMz8KfsDR9JX0lJWDRNc6nXX31v7ZEyg=="],
|
||||||
|
|
||||||
|
"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=="],
|
||||||
|
}
|
||||||
|
}
|
||||||
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"name": "hype",
|
||||||
|
"module": "src/index.tsx",
|
||||||
|
"exports": "./src/index.tsx",
|
||||||
|
"type": "module",
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/bun": "latest"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"hono": "^4.10.4",
|
||||||
|
"kleur": "^4.1.5"
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/index.tsx
Normal file
90
src/index.tsx
Normal file
|
|
@ -0,0 +1,90 @@
|
||||||
|
import { join } from 'path'
|
||||||
|
import { type Context, Hono } from 'hono'
|
||||||
|
import { serveStatic } from 'hono/bun'
|
||||||
|
import color from 'kleur'
|
||||||
|
|
||||||
|
import { transpile } from './utils'
|
||||||
|
|
||||||
|
const SHOW_HTTP_LOG = true
|
||||||
|
|
||||||
|
export class Hype extends Hono {
|
||||||
|
routesRegistered = false
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super()
|
||||||
|
}
|
||||||
|
|
||||||
|
registerRoutes() {
|
||||||
|
if (this.routesRegistered) return
|
||||||
|
this.routesRegistered = true
|
||||||
|
|
||||||
|
// static assets in pub/
|
||||||
|
this.use('/*', serveStatic({ root: './pub' }))
|
||||||
|
|
||||||
|
// css lives in src/, close to real code
|
||||||
|
this.use('/css/*', serveStatic({ root: './src' }))
|
||||||
|
|
||||||
|
// console logging
|
||||||
|
this.use('*', async (c, next) => {
|
||||||
|
if (!SHOW_HTTP_LOG) return await next()
|
||||||
|
|
||||||
|
const start = Date.now()
|
||||||
|
await next()
|
||||||
|
const end = Date.now()
|
||||||
|
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)
|
||||||
|
console.log(fn(`${c.res.status}`), `${color.bold(method)} ${c.req.url} (${end - start}ms)`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// serve transpiled js
|
||||||
|
this.on('GET', ['/js/:path{.+}', '/shared/:path{.+}'], async c => {
|
||||||
|
let path = './src/' + c.req.path.replace('..', '.')
|
||||||
|
|
||||||
|
// path must end in .js or .ts
|
||||||
|
if (!path.endsWith('.js') && !path.endsWith('.ts')) path += '.ts'
|
||||||
|
|
||||||
|
const ts = path.replace('.js', '.ts')
|
||||||
|
if (await Bun.file(ts).exists()) {
|
||||||
|
return new Response(await transpile(ts), { 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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`)
|
||||||
|
|
||||||
|
if (!(await Bun.file(path).exists()))
|
||||||
|
return render404(c)
|
||||||
|
|
||||||
|
const layoutPath = join(process.env.PWD ?? '.', `./src/pages/_layout.tsx`)
|
||||||
|
let Layout
|
||||||
|
if (await Bun.file(layoutPath).exists())
|
||||||
|
Layout = (await import(layoutPath + `?t=${Date.now()}`)).default
|
||||||
|
|
||||||
|
const page = await import(path + `?t=${Date.now()}`)
|
||||||
|
const innerHTML = typeof page.default === 'function' ? <page.default req={c.req} /> : page.default
|
||||||
|
const withLayout = Layout ? <Layout>{innerHTML}</Layout> : innerHTML
|
||||||
|
return c.html(withLayout)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
get defaults() {
|
||||||
|
this.registerRoutes()
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetch: this.fetch,
|
||||||
|
idleTimeout: 255
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function render404(c: Context) {
|
||||||
|
return c.text('File not found', 404)
|
||||||
|
}
|
||||||
130
src/utils.ts
Normal file
130
src/utils.ts
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { type Context } from 'hono'
|
||||||
|
import { stat } from 'fs/promises'
|
||||||
|
|
||||||
|
// lighten a hex color by blending with white. opacity 1 = original, 0 = white
|
||||||
|
export function lightenColor(hex: string, opacity: number): string {
|
||||||
|
// Remove # if present
|
||||||
|
hex = hex.replace('#', '')
|
||||||
|
|
||||||
|
// Convert to RGB
|
||||||
|
let r = parseInt(hex.substring(0, 2), 16)
|
||||||
|
let g = parseInt(hex.substring(2, 4), 16)
|
||||||
|
let b = parseInt(hex.substring(4, 6), 16)
|
||||||
|
|
||||||
|
// Blend with white
|
||||||
|
r = Math.round(r * opacity + 255 * (1 - opacity))
|
||||||
|
g = Math.round(g * opacity + 255 * (1 - opacity))
|
||||||
|
b = Math.round(b * opacity + 255 * (1 - opacity))
|
||||||
|
|
||||||
|
// Convert back to hex
|
||||||
|
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// darken a hex color by blending with black. opacity 1 = original, 0 = black
|
||||||
|
export function darkenColor(hex: string, opacity: number): string {
|
||||||
|
hex = hex.replace('#', '')
|
||||||
|
|
||||||
|
let r = parseInt(hex.substring(0, 2), 16)
|
||||||
|
let g = parseInt(hex.substring(2, 4), 16)
|
||||||
|
let b = parseInt(hex.substring(4, 6), 16)
|
||||||
|
|
||||||
|
// Blend with black (0, 0, 0)
|
||||||
|
r = Math.round(r * opacity)
|
||||||
|
g = Math.round(g * opacity)
|
||||||
|
b = Math.round(b * opacity)
|
||||||
|
|
||||||
|
return '#' + [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user prefers dark mode
|
||||||
|
export function isDarkMode(): boolean {
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
}
|
||||||
|
|
||||||
|
// capitalize a word. that's it.
|
||||||
|
export function capitalize(str: string): string {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a 6 character random ID
|
||||||
|
export function randomId(): string {
|
||||||
|
return Math.random().toString(36).slice(7)
|
||||||
|
}
|
||||||
|
|
||||||
|
// inclusive
|
||||||
|
// rand(2) == flip a coin
|
||||||
|
// rand(6) == roll a die
|
||||||
|
// rand(20) == dnd
|
||||||
|
export function rand(end = 2, startAtZero = false): number {
|
||||||
|
const start = startAtZero ? 0 : 1
|
||||||
|
return Math.floor(Math.random() * (end - start + 1)) + start
|
||||||
|
}
|
||||||
|
|
||||||
|
// randRange(1, 2) == flip a coin
|
||||||
|
// randRange(1, 6) == roll a die
|
||||||
|
// randRange(1, 20) == dnd
|
||||||
|
export function randRange(start = 0, end = 12): number {
|
||||||
|
return Math.floor(Math.random() * (end - start + 1)) + start
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomItem([5, 7, 9]) #=> 7
|
||||||
|
export function randItem<T>(list: T[]): T | undefined {
|
||||||
|
if (list.length === 0) return
|
||||||
|
return list[randRange(0, list.length - 1)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// randomIndex([5, 7, 9]) #=> 1
|
||||||
|
export function randIndex<T>(list: T[]): number | undefined {
|
||||||
|
if (!list.length) return
|
||||||
|
return randRange(0, list.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unique([1,1,2,2,3,3]) #=> [1,2,3]
|
||||||
|
export function unique<T>(array: T[]): T[] {
|
||||||
|
return [...new Set(array)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// random number between 1 and 10, with decreasing probability
|
||||||
|
export function weightedRand(): number {
|
||||||
|
// Weights: 1 has weight 10, 2 has weight 9, ..., 10 has weight 1
|
||||||
|
const weights = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
|
||||||
|
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||||
|
|
||||||
|
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0)
|
||||||
|
let random = Math.random() * totalWeight
|
||||||
|
|
||||||
|
for (let i = 0; i < weights.length; i++) {
|
||||||
|
random -= weights[i]!
|
||||||
|
if (random <= 0) {
|
||||||
|
return numbers[i]!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numbers[numbers.length - 1]!
|
||||||
|
}
|
||||||
|
|
||||||
|
const transpiler = new Bun.Transpiler({ loader: 'tsx' })
|
||||||
|
const transpileCache: Record<string, string> = {}
|
||||||
|
|
||||||
|
// Transpile the frontend *.ts file at `path` to JavaScript.
|
||||||
|
export async function transpile(path: string): Promise<string> {
|
||||||
|
const { mtime } = await stat(path)
|
||||||
|
const key = `${path}?${mtime}`
|
||||||
|
|
||||||
|
let cached = transpileCache[key]
|
||||||
|
if (!cached) {
|
||||||
|
const code = await Bun.file(path).text()
|
||||||
|
cached = transpiler.transformSync(code)
|
||||||
|
cached = cached.replaceAll(/\bjsxDEV_?\w*\(/g, "jsx(")
|
||||||
|
cached = cached.replaceAll(/\bFragment_?\w*,/g, "Fragment,")
|
||||||
|
|
||||||
|
transpileCache[key] = cached
|
||||||
|
}
|
||||||
|
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
// redirect to the referrer, or fallback if no referrer
|
||||||
|
export function redirectBack(c: Context, fallback = "/") {
|
||||||
|
return c.redirect(c.req.header("Referer") || fallback)
|
||||||
|
}
|
||||||
36
tsconfig.json
Normal file
36
tsconfig.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
// Environment setup & latest features
|
||||||
|
"lib": [
|
||||||
|
"ESNext",
|
||||||
|
"DOM"
|
||||||
|
],
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "Preserve",
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx",
|
||||||
|
"allowJs": true,
|
||||||
|
// Bundler mode
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"noEmit": true,
|
||||||
|
// Best practices
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"noImplicitOverride": true,
|
||||||
|
// Some stricter flags (disabled by default)
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"#*": [
|
||||||
|
"src/*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user