···
1
1
+
# dependencies (bun install)
2
2
+
node_modules
3
3
+
4
4
+
# output
5
5
+
out
6
6
+
dist
7
7
+
*.tgz
8
8
+
9
9
+
# code coverage
10
10
+
coverage
11
11
+
*.lcov
12
12
+
13
13
+
# logs
14
14
+
logs
15
15
+
_.log
16
16
+
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
17
17
+
18
18
+
# dotenv environment variable files
19
19
+
.env
20
20
+
.env.development.local
21
21
+
.env.test.local
22
22
+
.env.production.local
23
23
+
.env.local
24
24
+
25
25
+
# caches
26
26
+
.eslintcache
27
27
+
.cache
28
28
+
*.tsbuildinfo
29
29
+
30
30
+
# IntelliJ based IDEs
31
31
+
.idea
32
32
+
33
33
+
# Finder (MacOS) folder config
34
34
+
.DS_Store
···
1
1
+
2
2
+
Default to using Bun instead of Node.js.
3
3
+
4
4
+
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
5
5
+
- Use `bun test` instead of `jest` or `vitest`
6
6
+
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
7
7
+
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
8
8
+
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
9
9
+
- Use `bunx <package> <command>` instead of `npx <package> <command>`
10
10
+
- Bun automatically loads .env, so don't use dotenv.
11
11
+
12
12
+
## APIs
13
13
+
14
14
+
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
15
15
+
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
16
16
+
- `Bun.redis` for Redis. Don't use `ioredis`.
17
17
+
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
18
18
+
- `WebSocket` is built-in. Don't use `ws`.
19
19
+
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
20
20
+
- Bun.$`ls` instead of execa.
21
21
+
22
22
+
## Testing
23
23
+
24
24
+
Use `bun test` to run tests.
25
25
+
26
26
+
```ts#index.test.ts
27
27
+
import { test, expect } from "bun:test";
28
28
+
29
29
+
test("hello world", () => {
30
30
+
expect(1).toBe(1);
31
31
+
});
32
32
+
```
33
33
+
34
34
+
## Frontend
35
35
+
36
36
+
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
37
37
+
38
38
+
Server:
39
39
+
40
40
+
```ts#index.ts
41
41
+
import index from "./index.html"
42
42
+
43
43
+
Bun.serve({
44
44
+
routes: {
45
45
+
"/": index,
46
46
+
"/api/users/:id": {
47
47
+
GET: (req) => {
48
48
+
return new Response(JSON.stringify({ id: req.params.id }));
49
49
+
},
50
50
+
},
51
51
+
},
52
52
+
// optional websocket support
53
53
+
websocket: {
54
54
+
open: (ws) => {
55
55
+
ws.send("Hello, world!");
56
56
+
},
57
57
+
message: (ws, message) => {
58
58
+
ws.send(message);
59
59
+
},
60
60
+
close: (ws) => {
61
61
+
// handle close
62
62
+
}
63
63
+
},
64
64
+
development: {
65
65
+
hmr: true,
66
66
+
console: true,
67
67
+
}
68
68
+
})
69
69
+
```
70
70
+
71
71
+
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.
72
72
+
73
73
+
```html#index.html
74
74
+
<html>
75
75
+
<body>
76
76
+
<h1>Hello, world!</h1>
77
77
+
<script type="module" src="./frontend.tsx"></script>
78
78
+
</body>
79
79
+
</html>
80
80
+
```
81
81
+
82
82
+
With the following `frontend.tsx`:
83
83
+
84
84
+
```tsx#frontend.tsx
85
85
+
import React from "react";
86
86
+
import { createRoot } from "react-dom/client";
87
87
+
88
88
+
// import .css files directly and it works
89
89
+
import './index.css';
90
90
+
91
91
+
const root = createRoot(document.body);
92
92
+
93
93
+
export default function Frontend() {
94
94
+
return <h1>Hello, world!</h1>;
95
95
+
}
96
96
+
97
97
+
root.render(<Frontend />);
98
98
+
```
99
99
+
100
100
+
Then, run index.ts
101
101
+
102
102
+
```sh
103
103
+
bun --hot ./index.ts
104
104
+
```
105
105
+
106
106
+
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.
···
1
1
+
# watsup
2
2
+
3
3
+
To install dependencies:
4
4
+
5
5
+
```bash
6
6
+
bun install
7
7
+
```
8
8
+
9
9
+
To start a development server:
10
10
+
11
11
+
```bash
12
12
+
bun dev
13
13
+
```
14
14
+
15
15
+
To run for production:
16
16
+
17
17
+
```bash
18
18
+
bun start
19
19
+
```
20
20
+
21
21
+
This project was created using `bun init` in bun v1.3.7. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
···
1
1
+
#!/usr/bin/env bun
2
2
+
import plugin from "bun-plugin-tailwind";
3
3
+
import { existsSync } from "fs";
4
4
+
import { rm } from "fs/promises";
5
5
+
import path from "path";
6
6
+
7
7
+
if (process.argv.includes("--help") || process.argv.includes("-h")) {
8
8
+
console.log(`
9
9
+
🏗️ Bun Build Script
10
10
+
11
11
+
Usage: bun run build.ts [options]
12
12
+
13
13
+
Common Options:
14
14
+
--outdir <path> Output directory (default: "dist")
15
15
+
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
16
16
+
--sourcemap <type> Sourcemap type: none|linked|inline|external
17
17
+
--target <target> Build target: browser|bun|node
18
18
+
--format <format> Output format: esm|cjs|iife
19
19
+
--splitting Enable code splitting
20
20
+
--packages <type> Package handling: bundle|external
21
21
+
--public-path <path> Public path for assets
22
22
+
--env <mode> Environment handling: inline|disable|prefix*
23
23
+
--conditions <list> Package.json export conditions (comma separated)
24
24
+
--external <list> External packages (comma separated)
25
25
+
--banner <text> Add banner text to output
26
26
+
--footer <text> Add footer text to output
27
27
+
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
28
28
+
--help, -h Show this help message
29
29
+
30
30
+
Example:
31
31
+
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
32
32
+
`);
33
33
+
process.exit(0);
34
34
+
}
35
35
+
36
36
+
const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());
37
37
+
38
38
+
const parseValue = (value: string): any => {
39
39
+
if (value === "true") return true;
40
40
+
if (value === "false") return false;
41
41
+
42
42
+
if (/^\d+$/.test(value)) return parseInt(value, 10);
43
43
+
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);
44
44
+
45
45
+
if (value.includes(",")) return value.split(",").map(v => v.trim());
46
46
+
47
47
+
return value;
48
48
+
};
49
49
+
50
50
+
function parseArgs(): Partial<Bun.BuildConfig> {
51
51
+
const config: Record<string, unknown> = {};
52
52
+
const args = process.argv.slice(2);
53
53
+
54
54
+
for (let i = 0; i < args.length; i++) {
55
55
+
const arg = args[i];
56
56
+
if (arg === undefined) continue;
57
57
+
if (!arg.startsWith("--")) continue;
58
58
+
59
59
+
if (arg.startsWith("--no-")) {
60
60
+
const key = toCamelCase(arg.slice(5));
61
61
+
config[key] = false;
62
62
+
continue;
63
63
+
}
64
64
+
65
65
+
if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
66
66
+
const key = toCamelCase(arg.slice(2));
67
67
+
config[key] = true;
68
68
+
continue;
69
69
+
}
70
70
+
71
71
+
let key: string;
72
72
+
let value: string;
73
73
+
74
74
+
if (arg.includes("=")) {
75
75
+
[key, value] = arg.slice(2).split("=", 2) as [string, string];
76
76
+
} else {
77
77
+
key = arg.slice(2);
78
78
+
value = args[++i] ?? "";
79
79
+
}
80
80
+
81
81
+
key = toCamelCase(key);
82
82
+
83
83
+
if (key.includes(".")) {
84
84
+
const parts = key.split(".");
85
85
+
if (parts.length > 2) {
86
86
+
console.warn(
87
87
+
`Warning: Deeply nested option "${key}" is not supported. Only single-level nesting (e.g., --minify.whitespace) is allowed.`,
88
88
+
);
89
89
+
continue;
90
90
+
}
91
91
+
const parentKey = parts[0]!;
92
92
+
const childKey = parts[1]!;
93
93
+
const existing = config[parentKey];
94
94
+
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
95
95
+
config[parentKey] = {};
96
96
+
}
97
97
+
(config[parentKey] as Record<string, unknown>)[childKey] = parseValue(value);
98
98
+
} else {
99
99
+
config[key] = parseValue(value);
100
100
+
}
101
101
+
}
102
102
+
103
103
+
return config as Partial<Bun.BuildConfig>;
104
104
+
}
105
105
+
106
106
+
const formatFileSize = (bytes: number): string => {
107
107
+
const units = ["B", "KB", "MB", "GB"];
108
108
+
let size = bytes;
109
109
+
let unitIndex = 0;
110
110
+
111
111
+
while (size >= 1024 && unitIndex < units.length - 1) {
112
112
+
size /= 1024;
113
113
+
unitIndex++;
114
114
+
}
115
115
+
116
116
+
return `${size.toFixed(2)} ${units[unitIndex]}`;
117
117
+
};
118
118
+
119
119
+
console.log("\n🚀 Starting build process...\n");
120
120
+
121
121
+
const cliConfig = parseArgs();
122
122
+
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");
123
123
+
124
124
+
if (existsSync(outdir)) {
125
125
+
console.log(`🗑️ Cleaning previous build at ${outdir}`);
126
126
+
await rm(outdir, { recursive: true, force: true });
127
127
+
}
128
128
+
129
129
+
const start = performance.now();
130
130
+
131
131
+
const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
132
132
+
.map(a => path.resolve("src", a))
133
133
+
.filter(dir => !dir.includes("node_modules"));
134
134
+
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);
135
135
+
136
136
+
const result = await Bun.build({
137
137
+
entrypoints,
138
138
+
outdir,
139
139
+
plugins: [plugin],
140
140
+
minify: true,
141
141
+
target: "browser",
142
142
+
sourcemap: "linked",
143
143
+
define: {
144
144
+
"process.env.NODE_ENV": JSON.stringify("production"),
145
145
+
},
146
146
+
...cliConfig,
147
147
+
});
148
148
+
149
149
+
const end = performance.now();
150
150
+
151
151
+
const outputTable = result.outputs.map(output => ({
152
152
+
File: path.relative(process.cwd(), output.path),
153
153
+
Type: output.kind,
154
154
+
Size: formatFileSize(output.size),
155
155
+
}));
156
156
+
157
157
+
console.table(outputTable);
158
158
+
const buildTime = (end - start).toFixed(2);
159
159
+
160
160
+
console.log(`\n✅ Build completed in ${buildTime}ms\n`);
···
1
1
+
// Generated by `bun init`
2
2
+
3
3
+
declare module "*.svg" {
4
4
+
/**
5
5
+
* A path to the SVG file
6
6
+
*/
7
7
+
const path: `${string}.svg`;
8
8
+
export = path;
9
9
+
}
10
10
+
11
11
+
declare module "*.module.css" {
12
12
+
/**
13
13
+
* A record of class names to their corresponding CSS module classes
14
14
+
*/
15
15
+
const classes: { readonly [key: string]: string };
16
16
+
export = classes;
17
17
+
}
···
1
1
+
{
2
2
+
"lockfileVersion": 1,
3
3
+
"configVersion": 1,
4
4
+
"workspaces": {
5
5
+
"": {
6
6
+
"name": "bun-react-template",
7
7
+
"dependencies": {
8
8
+
"bun-plugin-tailwind": "^0.1.2",
9
9
+
"lucide-react": "^0.563.0",
10
10
+
"react": "^19",
11
11
+
"react-dom": "^19",
12
12
+
"tailwindcss": "^4.1.11",
13
13
+
},
14
14
+
"devDependencies": {
15
15
+
"@types/bun": "latest",
16
16
+
"@types/react": "^19",
17
17
+
"@types/react-dom": "^19",
18
18
+
},
19
19
+
},
20
20
+
},
21
21
+
"packages": {
22
22
+
"@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Mh78f4B+vNTOhFpI7RWHRWDqSKTnFXj/MauRx7I/GmNwEfw56sUx98gWRwXyF4lkW+9VNU+33wuw6E+M22W66w=="],
23
23
+
24
24
+
"@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-dFfKdSVz6Ois5zjEJboUC7igcYAVd+c//ajotd0L6WUQAKQrHMVq/+6LjOj/0zjC6VPFNGWzeF8erymNo1y0Jw=="],
25
25
+
26
26
+
"@oven/bun-darwin-x64-baseline": ["@oven/bun-darwin-x64-baseline@1.3.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-bUND1aQoTCfIL+idALT7FWtuX59ltOIRo954c7p/JkESbSIJ01jY06BSNVbkGk8RQM19v/7qiqZZqi4NyO4Utw=="],
27
27
+
28
28
+
"@oven/bun-linux-aarch64": ["@oven/bun-linux-aarch64@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-m03OtzEs+/RkWtk6tBf8yw0GW4P8ajfzTXnTt984tQBgkMubGQYUyUnFasWgr3mD2820LhkVjhYeBf1rkz/biQ=="],
29
29
+
30
30
+
"@oven/bun-linux-aarch64-musl": ["@oven/bun-linux-aarch64-musl@1.3.7", "", { "os": "linux", "cpu": "arm64" }, "sha512-QDxrROdUnC1d/uoilXtUeFHaLhYdRN7dRIzw/Iqj/vrrhnkA6VS+HYoCWtyyVvci/K+JrPmDwxOWlSRpmV4INA=="],
31
31
+
32
32
+
"@oven/bun-linux-x64": ["@oven/bun-linux-x64@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-uttKQ/eIRVGc4uBtLRqmQqXGf57/dmQaF0AEd37RQNRRRd1P/VYnFMiMcVaot3HJ6IFjHjGtcPO9ekT49LxBYQ=="],
33
33
+
34
34
+
"@oven/bun-linux-x64-baseline": ["@oven/bun-linux-x64-baseline@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-Jlb/AcrIFU3QDeR3EL4UVT1CIKqnLJDgbU+R0k/+NaSWMrBEpZV+gJJT5L1cmEKTNhU/d+c7hudxkjtqA7XXqA=="],
35
35
+
36
36
+
"@oven/bun-linux-x64-musl": ["@oven/bun-linux-x64-musl@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-aK8fvkCosrHRG3CNdVqMom1C8Rj3XkqZp0ZFSBXgaXlKP22RkxlEE9tS7OmSq9yVgEk6euTB3dW4NFo/jlXqeg=="],
37
37
+
38
38
+
"@oven/bun-linux-x64-musl-baseline": ["@oven/bun-linux-x64-musl-baseline@1.3.7", "", { "os": "linux", "cpu": "x64" }, "sha512-lySQQ7zJJsoa5hQH+PE5bQyQaTI8G2Erszhu4iQuDtsocwy3zSxjB6TxGWTd4HmetPl9aRvg3nb2KR8RVAd7ug=="],
39
39
+
40
40
+
"@oven/bun-windows-x64": ["@oven/bun-windows-x64@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-3QdIGdSn3fkssCq/vPjtPLAQxo+eMUzcwJedn1c5mXDy1AoisjhoxhWnbVl8+uk+wt9N6JUPdISoe0N4OdwXfg=="],
41
41
+
42
42
+
"@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.7", "", { "os": "win32", "cpu": "x64" }, "sha512-wMgELfW5vFceh4qEOYb5iV5TjrjjnBJzE383ixA3kqGKzaubksSxNc11eZhS0ptcJ5a0UjN5hfbMh6sYoh+cRQ=="],
43
43
+
44
44
+
"@types/bun": ["@types/bun@1.3.7", "", { "dependencies": { "bun-types": "1.3.7" } }, "sha512-lmNuMda+Z9b7tmhA0tohwy8ZWFSnmQm1UDWXtH5r9F7wZCfkeO3Jx7wKQ1EOiKq43yHts7ky6r8SDJQWRNupkA=="],
45
45
+
46
46
+
"@types/node": ["@types/node@25.1.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA=="],
47
47
+
48
48
+
"@types/react": ["@types/react@19.2.10", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw=="],
49
49
+
50
50
+
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="],
51
51
+
52
52
+
"bun": ["bun@1.3.7", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.7", "@oven/bun-darwin-x64": "1.3.7", "@oven/bun-darwin-x64-baseline": "1.3.7", "@oven/bun-linux-aarch64": "1.3.7", "@oven/bun-linux-aarch64-musl": "1.3.7", "@oven/bun-linux-x64": "1.3.7", "@oven/bun-linux-x64-baseline": "1.3.7", "@oven/bun-linux-x64-musl": "1.3.7", "@oven/bun-linux-x64-musl-baseline": "1.3.7", "@oven/bun-windows-x64": "1.3.7", "@oven/bun-windows-x64-baseline": "1.3.7" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-ha86NG8WiAXYR7eQw/9S+7V7Lo8KfD36XutWJNS1VndzaipWS0QIen5n3K9MT3PpP/sdGmmHjhkrU0sCM2lGGQ=="],
53
53
+
54
54
+
"bun-plugin-tailwind": ["bun-plugin-tailwind@0.1.2", "", { "peerDependencies": { "bun": ">=1.0.0" } }, "sha512-41jNC1tZRSK3s1o7pTNrLuQG8kL/0vR/JgiTmZAJ1eHwe0w5j6HFPKeqEk0WAD13jfrUC7+ULuewFBBCoADPpg=="],
55
55
+
56
56
+
"bun-types": ["bun-types@1.3.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-qyschsA03Qz+gou+apt6HNl6HnI+sJJLL4wLDke4iugsE6584CMupOtTY1n+2YC9nGVrEKUlTs99jjRLKgWnjQ=="],
57
57
+
58
58
+
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
59
59
+
60
60
+
"lucide-react": ["lucide-react@0.563.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA=="],
61
61
+
62
62
+
"react": ["react@19.2.4", "", {}, "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ=="],
63
63
+
64
64
+
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ=="],
65
65
+
66
66
+
"scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
67
67
+
68
68
+
"tailwindcss": ["tailwindcss@4.1.18", "", {}, "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw=="],
69
69
+
70
70
+
"undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
71
71
+
}
72
72
+
}
···
1
1
+
2
2
+
[serve.static]
3
3
+
plugins = ["bun-plugin-tailwind"]
4
4
+
env = "BUN_PUBLIC_*"
···
1
1
+
{
2
2
+
"name": "bun-react-template",
3
3
+
"version": "0.1.0",
4
4
+
"private": true,
5
5
+
"type": "module",
6
6
+
"scripts": {
7
7
+
"dev": "bun --hot src/index.ts",
8
8
+
"start": "NODE_ENV=production bun src/index.ts",
9
9
+
"build": "bun run build.ts"
10
10
+
},
11
11
+
"dependencies": {
12
12
+
"bun-plugin-tailwind": "^0.1.2",
13
13
+
"lucide-react": "^0.563.0",
14
14
+
"react": "^19",
15
15
+
"react-dom": "^19",
16
16
+
"tailwindcss": "^4.1.11"
17
17
+
},
18
18
+
"devDependencies": {
19
19
+
"@types/react": "^19",
20
20
+
"@types/react-dom": "^19",
21
21
+
"@types/bun": "latest"
22
22
+
}
23
23
+
}
···
1
1
+
import Layout from "./Layout";
2
2
+
import ServiceGrid from "./components/ServiceGrid";
3
3
+
import { apps } from "./data/apps";
4
4
+
import { privateApps } from "./data/privateApps";
5
5
+
import { websites } from "./data/websites";
6
6
+
7
7
+
export function App() {
8
8
+
return (
9
9
+
<Layout>
10
10
+
<h2 className="text-2xl font-semibold text-zinc-400 mt-8 mb-4">
11
11
+
Websites
12
12
+
</h2>
13
13
+
<ServiceGrid services={websites} columns={2} />
14
14
+
<h2 className="text-2xl font-semibold text-zinc-400 mt-8 mb-4">
15
15
+
Public Apps
16
16
+
</h2>
17
17
+
<ServiceGrid services={apps} columns={4} />
18
18
+
<h2 className="text-2xl font-semibold text-zinc-400 mt-8 mb-4">
19
19
+
Tailnet Apps
20
20
+
</h2>
21
21
+
<ServiceGrid services={privateApps} columns={4} />
22
22
+
</Layout>
23
23
+
);
24
24
+
}
25
25
+
26
26
+
export default App;
···
1
1
+
import "./index.css";
2
2
+
3
3
+
export default function Layout({ children }: { children: React.ReactNode }) {
4
4
+
return (
5
5
+
<main className="min-h-screen p-8 max-w-5xl mx-auto">
6
6
+
{/*<header className="mb-12">
7
7
+
<h1 className="text-4xl font-bold">cute.haus</h1>
8
8
+
</header>*/}
9
9
+
{children}
10
10
+
</main>
11
11
+
);
12
12
+
}
···
1
1
+
import { useEffect, useState } from "react";
2
2
+
import { Circle, CircleCheck, CircleX } from "lucide-react";
3
3
+
import type { Service } from "../types";
4
4
+
5
5
+
type Props = {
6
6
+
services: Service[];
7
7
+
columns?: 1 | 2 | 3 | 4;
8
8
+
refreshInterval?: number;
9
9
+
};
10
10
+
11
11
+
export default function ServiceGrid({
12
12
+
services,
13
13
+
columns = 4,
14
14
+
refreshInterval = 20000,
15
15
+
}: Props) {
16
16
+
const [statuses, setStatuses] = useState<Record<string, boolean | null>>({});
17
17
+
18
18
+
useEffect(() => {
19
19
+
const fetchStatuses = () => {
20
20
+
fetch("/api/check", {
21
21
+
method: "POST",
22
22
+
headers: {
23
23
+
"Content-Type": "application/json",
24
24
+
},
25
25
+
body: JSON.stringify(services),
26
26
+
})
27
27
+
.then((response) => response.json())
28
28
+
.then((data) => {
29
29
+
setStatuses(data);
30
30
+
});
31
31
+
};
32
32
+
33
33
+
fetchStatuses();
34
34
+
const interval = setInterval(fetchStatuses, refreshInterval);
35
35
+
36
36
+
return () => clearInterval(interval);
37
37
+
}, [services, refreshInterval]);
38
38
+
39
39
+
const gridCols = {
40
40
+
1: "md:grid-cols-1",
41
41
+
2: "md:grid-cols-2",
42
42
+
3: "md:grid-cols-3",
43
43
+
4: "md:grid-cols-4",
44
44
+
};
45
45
+
46
46
+
return (
47
47
+
<div className={`grid gap-4 ${gridCols[columns]}`}>
48
48
+
{services.map((service) => (
49
49
+
<a
50
50
+
key={service.name}
51
51
+
href={service.url}
52
52
+
className="block p-4 bg-zinc-800 border border-zinc-700 rounded-lg hover:bg-zinc-750 hover:border-zinc-600 hover:scale-105 transition-all"
53
53
+
>
54
54
+
<div className="flex items-center gap-3">
55
55
+
{service.icon && (
56
56
+
<img src={service.icon} alt="" className="w-6 h-6 shrink-0" />
57
57
+
)}
58
58
+
<h3 className="text-lg font-semibold flex-1 truncate">
59
59
+
{service.name}
60
60
+
</h3>
61
61
+
<span className="shrink-0">
62
62
+
{statuses[service.name] == null && (
63
63
+
<Circle className="text-zinc-500" />
64
64
+
)}
65
65
+
{statuses[service.name] === true && (
66
66
+
<CircleCheck className="text-emerald-400" />
67
67
+
)}
68
68
+
{statuses[service.name] === false && (
69
69
+
<CircleX className="text-rose-400" />
70
70
+
)}
71
71
+
</span>
72
72
+
</div>
73
73
+
</a>
74
74
+
))}
75
75
+
</div>
76
76
+
);
77
77
+
}
···
1
1
+
import type { Service } from "../types";
2
2
+
3
3
+
export const apps: Service[] = [
4
4
+
{
5
5
+
name: "Plex",
6
6
+
url: "https://plex.cute.haus/",
7
7
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/plex.png",
8
8
+
},
9
9
+
{
10
10
+
name: "Ombi",
11
11
+
url: "https://ombi.cute.haus/",
12
12
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/ombi.png",
13
13
+
},
14
14
+
{
15
15
+
name: "Audiobookshelf",
16
16
+
url: "https://audiobookshelf.cute.haus/",
17
17
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/audiobookshelf.png",
18
18
+
},
19
19
+
{
20
20
+
name: "Immich",
21
21
+
url: "https://immich.cute.haus/",
22
22
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/immich.png",
23
23
+
},
24
24
+
{
25
25
+
name: "Forĝejo",
26
26
+
url: "https://git.aly.codes/",
27
27
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/forgejo.png",
28
28
+
},
29
29
+
{
30
30
+
name: "Karakeep",
31
31
+
url: "https://karakeep.cute.haus/",
32
32
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/karakeep.png",
33
33
+
},
34
34
+
{
35
35
+
name: "aly.social",
36
36
+
url: "https://aly.social/",
37
37
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/bluesky.png",
38
38
+
},
39
39
+
{
40
40
+
name: "Vaultwarden",
41
41
+
url: "https://vault.cute.haus/",
42
42
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/vaultwarden.png",
43
43
+
},
44
44
+
];
···
1
1
+
import type { Service } from "../types";
2
2
+
3
3
+
export const privateApps: Service[] = [
4
4
+
{
5
5
+
name: "Jellyfin",
6
6
+
url: "https://jellyfin.narwhal-snapper.ts.net/",
7
7
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/jellyfin.png",
8
8
+
},
9
9
+
{
10
10
+
name: "Photoprism",
11
11
+
url: "https://photoprism.narwhal-snapper.ts.net/",
12
12
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/photoprism.png",
13
13
+
},
14
14
+
{
15
15
+
name: "Navidrome",
16
16
+
url: "https://navidrome.narwhal-snapper.ts.net/",
17
17
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/navidrome.png",
18
18
+
},
19
19
+
{
20
20
+
name: "Sonarr",
21
21
+
url: "https://sonarr.narwhal-snapper.ts.net/",
22
22
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/sonarr.png",
23
23
+
},
24
24
+
{
25
25
+
name: "Radarr",
26
26
+
url: "https://radarr.narwhal-snapper.ts.net/",
27
27
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/radarr.png",
28
28
+
},
29
29
+
{
30
30
+
name: "Lidarr",
31
31
+
url: "https://lidarr.narwhal-snapper.ts.net/",
32
32
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/lidarr.png",
33
33
+
},
34
34
+
{
35
35
+
name: "Prowlarr",
36
36
+
url: "https://prowlarr.narwhal-snapper.ts.net/",
37
37
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/prowlarr.png",
38
38
+
},
39
39
+
{
40
40
+
name: "Bazarr",
41
41
+
url: "https://bazarr.narwhal-snapper.ts.net/",
42
42
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/bazarr.png",
43
43
+
},
44
44
+
{
45
45
+
name: "Tautulli",
46
46
+
url: "https://tautulli.narwhal-snapper.ts.net/",
47
47
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/tautulli.png",
48
48
+
},
49
49
+
{
50
50
+
name: "qBittorrent",
51
51
+
url: "https://qbittorrent.narwhal-snapper.ts.net/",
52
52
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/qbittorrent.png",
53
53
+
},
54
54
+
{
55
55
+
name: "Grafana",
56
56
+
url: "https://grafana.narwhal-snapper.ts.net/",
57
57
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/grafana.png",
58
58
+
},
59
59
+
{
60
60
+
name: "Uptime Kuma",
61
61
+
url: "https://uptime-kuma.narwhal-snapper.ts.net/",
62
62
+
icon: "https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png/uptime-kuma.png",
63
63
+
},
64
64
+
];
···
1
1
+
import type { Service } from "../types";
2
2
+
3
3
+
export const websites: Service[] = [
4
4
+
{ name: "aly.codes", url: "https://aly.codes/" },
5
5
+
{ name: "switchyard.aly.codes", url: "https://switchyard.aly.codes/" },
6
6
+
];
···
1
1
+
/**
2
2
+
* This file is the entry point for the React app, it sets up the root
3
3
+
* element and renders the App component to the DOM.
4
4
+
*
5
5
+
* It is included in `src/index.html`.
6
6
+
*/
7
7
+
8
8
+
import { createRoot } from "react-dom/client";
9
9
+
import { App } from "./App";
10
10
+
11
11
+
function start() {
12
12
+
const root = createRoot(document.getElementById("root")!);
13
13
+
root.render(<App />);
14
14
+
}
15
15
+
16
16
+
if (document.readyState === "loading") {
17
17
+
document.addEventListener("DOMContentLoaded", start);
18
18
+
} else {
19
19
+
start();
20
20
+
}
···
1
1
+
@import "tailwindcss";
···
1
1
+
<!doctype html>
2
2
+
<html lang="en">
3
3
+
<head>
4
4
+
<meta charset="UTF-8" />
5
5
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
+
<!--<link rel="icon" type="image/svg+xml" href="./logo.svg" />-->
7
7
+
<title>watsup</title>
8
8
+
</head>
9
9
+
<body class="bg-zinc-900 text-zinc-100">
10
10
+
<div id="root"></div>
11
11
+
<script type="module" src="./frontend.tsx"></script>
12
12
+
</body>
13
13
+
</html>
···
1
1
+
import { serve } from "bun";
2
2
+
import index from "./index.html";
3
3
+
import { privateApps } from "./data/privateApps";
4
4
+
import { websites } from "./data/websites";
5
5
+
6
6
+
async function checkStatuses(items: { name: string; url: string }[]) {
7
7
+
const results = await Promise.all(
8
8
+
items.map(async (item) => {
9
9
+
try {
10
10
+
const response = await fetch(item.url, {
11
11
+
method: "HEAD",
12
12
+
signal: AbortSignal.timeout(3000),
13
13
+
});
14
14
+
return { name: item.name, online: response.status < 500 };
15
15
+
} catch {
16
16
+
return { name: item.name, online: false };
17
17
+
}
18
18
+
}),
19
19
+
);
20
20
+
21
21
+
const statuses: Record<string, boolean> = {};
22
22
+
for (const result of results) {
23
23
+
statuses[result.name] = result.online;
24
24
+
}
25
25
+
return statuses;
26
26
+
}
27
27
+
28
28
+
const server = serve({
29
29
+
routes: {
30
30
+
// Serve index.html for all unmatched routes.
31
31
+
"/*": index,
32
32
+
33
33
+
"/api/check": {
34
34
+
async POST(req) {
35
35
+
const items = await req.json();
36
36
+
return Response.json(await checkStatuses(items));
37
37
+
},
38
38
+
},
39
39
+
40
40
+
"/api/websites": {
41
41
+
async GET(req) {
42
42
+
return Response.json(await checkStatuses(websites));
43
43
+
},
44
44
+
},
45
45
+
46
46
+
"/api/hello": {
47
47
+
async GET(req) {
48
48
+
return Response.json({
49
49
+
message: "Hello, world!",
50
50
+
method: "GET",
51
51
+
});
52
52
+
},
53
53
+
async PUT(req) {
54
54
+
return Response.json({
55
55
+
message: "Hello, world!",
56
56
+
method: "PUT",
57
57
+
});
58
58
+
},
59
59
+
},
60
60
+
61
61
+
"/api/hello/:name": async (req) => {
62
62
+
const name = req.params.name;
63
63
+
return Response.json({
64
64
+
message: `Hello, ${name}!`,
65
65
+
});
66
66
+
},
67
67
+
},
68
68
+
69
69
+
development: process.env.NODE_ENV !== "production" && {
70
70
+
// Enable browser hot reloading in development
71
71
+
hmr: true,
72
72
+
73
73
+
// Echo console logs from the browser to the server
74
74
+
console: true,
75
75
+
},
76
76
+
});
77
77
+
78
78
+
console.log(`🚀 Server running at ${server.url}`);
···
1
1
+
export type Service = { name: string; url: string; icon?: string };
···
1
1
+
{
2
2
+
"compilerOptions": {
3
3
+
// Environment setup & latest features
4
4
+
"lib": ["ESNext", "DOM"],
5
5
+
"target": "ESNext",
6
6
+
"module": "Preserve",
7
7
+
"moduleDetection": "force",
8
8
+
"jsx": "react-jsx",
9
9
+
"allowJs": true,
10
10
+
11
11
+
// Bundler mode
12
12
+
"moduleResolution": "bundler",
13
13
+
"allowImportingTsExtensions": true,
14
14
+
"verbatimModuleSyntax": true,
15
15
+
"noEmit": true,
16
16
+
17
17
+
// Best practices
18
18
+
"strict": true,
19
19
+
"skipLibCheck": true,
20
20
+
"noFallthroughCasesInSwitch": true,
21
21
+
"noUncheckedIndexedAccess": true,
22
22
+
"noImplicitOverride": true,
23
23
+
24
24
+
"baseUrl": ".",
25
25
+
"paths": {
26
26
+
"@/*": ["./src/*"]
27
27
+
},
28
28
+
29
29
+
// Some stricter flags (disabled by default)
30
30
+
"noUnusedLocals": false,
31
31
+
"noUnusedParameters": false,
32
32
+
"noPropertyAccessFromIndexSignature": false
33
33
+
},
34
34
+
35
35
+
"exclude": ["dist", "node_modules"]
36
36
+
}