From 931bb597f4c8c122e35e47c2765335f84bdf7c07 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath <2+defunkt@users.noreply.github.com> Date: Mon, 15 Sep 2025 13:13:41 -0700 Subject: [PATCH] basics --- .../use-bun-instead-of-node-vite-npm-pnpm.mdc | 1 + .gitignore | 34 ++++++ CLAUDE.md | 111 ++++++++++++++++++ bun.lock | 37 ++++++ package.json | 20 ++++ public/img/.gitignore | 0 public/vendor/.gitignore | 0 src/server.ts | 78 ++++++++++++ src/utils.tsx | 47 ++++++++ tsconfig.json | 29 +++++ 10 files changed, 357 insertions(+) create mode 120000 .cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 bun.lock create mode 100644 package.json create mode 100644 public/img/.gitignore create mode 100644 public/vendor/.gitignore create mode 100644 src/server.ts create mode 100644 src/utils.tsx create mode 100644 tsconfig.json diff --git a/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc new file mode 120000 index 0000000..6100270 --- /dev/null +++ b/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc @@ -0,0 +1 @@ +../../CLAUDE.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..b8100b7 --- /dev/null +++ b/CLAUDE.md @@ -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 ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +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

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`. diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..c551850 --- /dev/null +++ b/bun.lock @@ -0,0 +1,37 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "pluto", + "dependencies": { + "hono": "^4.9.7", + "kleur": "^4.1.5", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.22", "", { "dependencies": { "bun-types": "1.2.22" } }, "sha512-5A/KrKos2ZcN0c6ljRSOa1fYIyCKhZfIVYeuyb4snnvomnpFqC0tTsEkdqNxbAgExV384OETQ//WAjl3XbYqQA=="], + + "@types/node": ["@types/node@24.5.0", "", { "dependencies": { "undici-types": "~7.12.0" } }, "sha512-y1dMvuvJspJiPSDZUQ+WMBvF7dpnEqN4x9DDC9ie5Fs/HUZJA3wFp7EhHoVaKX/iI0cRoECV8X2jL8zi0xrHCg=="], + + "@types/react": ["@types/react@19.1.13", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ=="], + + "bun-types": ["bun-types@1.2.22", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "hono": ["hono@4.9.7", "", {}, "sha512-t4Te6ERzIaC48W3x4hJmBwgNlLhmiEdEE5ViYb02ffw4ignHNHa5IBtPjmbKstmtKa8X6C35iWwK4HaqvrzG9w=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.12.0", "", {}, "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..25b7495 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "pluto", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "start": "bun src/server.ts", + "dev": "bun --hot src/server.ts" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "hono": "^4.9.7", + "kleur": "^4.1.5" + } +} diff --git a/public/img/.gitignore b/public/img/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/public/vendor/.gitignore b/public/vendor/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..ad20a6f --- /dev/null +++ b/src/server.ts @@ -0,0 +1,78 @@ +import { Hono } from "hono" +import { serveStatic } from "hono/bun" +import { prettyJSON } from "hono/pretty-json" +import color from "kleur" + +import { transpile, isFile } from "./utils" + +export const NOSE_ICON = ` ͡° ͜ʖ ͡°` + +// +// Hono setup +// + +const app = new Hono() + +app.use("*", prettyJSON()) + +app.use('/img/*', serveStatic({ root: './public' })) +app.use('/vendor/*', serveStatic({ root: './public' })) +app.use('/css/*', serveStatic({ root: './src' })) + +app.use("*", async (c, next) => { + const start = Date.now() + await next() + const end = Date.now() + const fn = c.res.status < 300 ? color.green : c.res.status < 500 ? color.yellow : color.red + console.log(fn(`${c.res.status} ${c.req.method} ${c.req.url} (${end - start}ms)`)) +}) + +app.get("/js/:path{.+}", async c => { + const path = "./src/js/" + c.req.param("path") + const ts = path.replace(".js", ".ts") + if (isFile(ts)) { + return new Response(await transpile(ts), { headers: { "Content-Type": "text/javascript" } }) + } else if (isFile(path)) { + return new Response(Bun.file(path), { headers: { "Content-Type": "text/javascript" } }) + } else { + return c.text("File not found", 404) + } +}) + +// +// app routes +// + + + +// +// server shutdown +// + +// @ts-ignore +globalThis.__nose_cleanup?.() + +// @ts-ignore +globalThis.__nose_cleanup = () => { +} + +for (const sig of ["SIGINT", "SIGTERM"] as const) { + process.on(sig, () => { + // @ts-ignore + globalThis.__nose_cleanup?.() + process.exit() + }) +} + + +// +// server start +// + +console.log(color.cyan(NOSE_ICON)) + +export default { + port: process.env.PORT || 3000, + fetch: app.fetch, + idleTimeout: 0, +} \ No newline at end of file diff --git a/src/utils.tsx b/src/utils.tsx new file mode 100644 index 0000000..844a5ea --- /dev/null +++ b/src/utils.tsx @@ -0,0 +1,47 @@ +import { statSync } from "node:fs" +import { stat } from "node:fs/promises" + +// Is the given `path` a file? +export function isFile(path: string): boolean { + try { + const stats = statSync(path) + return stats.isFile() + } catch { + return false + } +} + +// Is the given `path` a directory? +export function isDir(path: string): boolean { + try { + const stats = statSync(path) + return stats.isDirectory() + } catch { + return false + } +} + +// Generate a random 8 character string +export function randomID(): string { + return Math.random().toString(36).slice(2, 10) +} + + +const transpiler = new Bun.Transpiler({ loader: 'tsx' }) +const transpileCache: Record = {} + +// Transpile the frontend *.ts file at `path` to JavaScript. +export async function transpile(path: string): Promise { + const code = await Bun.file(path).text() + + const { mtime } = await stat(path) + const key = `${path}?${mtime}` + + let cached = transpileCache[key] + if (!cached) { + cached = transpiler.transformSync(code) + transpileCache[key] = cached + } + + return cached +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-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 + } +}