├── bun.lockb ├── static └── css │ ├── input.css │ └── output.css ├── tailwind.config.js ├── README.md ├── index.tsx ├── package.json ├── tsconfig.json └── src ├── templates └── templates.tsx ├── middleware └── middleware.ts └── handlers └── handlers.tsx /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Phillip-England/bun-quickstart/main/bun.lockb -------------------------------------------------------------------------------- /static/css/input.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./index.ts", "./src/**/*.ts"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | plugins: [], 8 | } 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bun-quickstart 2 | 3 | To install dependencies: 4 | 5 | ```bash 6 | bun install 7 | ``` 8 | 9 | To run: 10 | 11 | ```bash 12 | bun run index.ts 13 | ``` 14 | 15 | This project was created using `bun init` in bun v1.1.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. 16 | -------------------------------------------------------------------------------- /index.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type { Serve } from "bun"; 3 | import { chain, html } from "./src/middleware/middleware"; 4 | import { handleHome, handleStatic, handleFavicon } from "./src/handlers/handlers"; 5 | 6 | let serverOptions: Serve = { 7 | port: 8080, 8 | async fetch(req) { 9 | const path = new URL(req.url).pathname; 10 | if (path.startsWith("/static/")) return await handleStatic(path); 11 | if (path == "/favicon.ico") return await handleFavicon(path) 12 | if (path === "/") return await chain(req, handleHome, html) 13 | return new Response("404!"); 14 | }, 15 | } 16 | 17 | Bun.serve(serverOptions); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bun-quickstart", 3 | "module": "index.tsx", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "peerDependencies": { 9 | "typescript": "^5.0.0" 10 | }, 11 | "scripts": { 12 | "dev": "bun --hot run index.tsx", 13 | "tailwind": "tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch" 14 | }, 15 | "dependencies": { 16 | "@types/react": "^18.3.1", 17 | "@types/react-dom": "^18.3.0", 18 | "react": "^18.3.1", 19 | "react-dom": "^18.3.1", 20 | "react-dom/server": "react-dom/server" 21 | } 22 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/templates/templates.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react" 2 | import ReactDOMServer from "react-dom/server" 3 | 4 | 5 | type BaseTemplateProps = { 6 | title: string 7 | children: ReactNode 8 | 9 | } 10 | 11 | export function BaseTemplate({title, children}: BaseTemplateProps): JSX.Element { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | {title} 19 | 20 | 21 | {children} 22 | 23 | 24 | ) 25 | } 26 | 27 | export function HomeTemplate(): JSX.Element { 28 | return ( 29 | 30 |

Hello World

31 |
32 | ) 33 | } -------------------------------------------------------------------------------- /src/middleware/middleware.ts: -------------------------------------------------------------------------------- 1 | export type CustomContext = { 2 | startTime: number 3 | headers: Headers 4 | } 5 | 6 | type CustomHandler = (req: Request, ctx: CustomContext) => Promise 7 | 8 | type CustomMiddleware = (req: Request, ctx: CustomContext) => Promise 9 | 10 | export async function chain(req: Request, handler: CustomHandler, ...middlewares: CustomMiddleware[]): Promise { 11 | const ctx: CustomContext = { 12 | startTime: Date.now(), 13 | headers: new Headers() 14 | } 15 | for (const middleware of middlewares) { 16 | let res = await middleware(req, ctx) 17 | if (res) return res 18 | } 19 | let res = await handler(req, ctx) 20 | log(req, ctx) 21 | return res 22 | } 23 | 24 | export function log(req: Request, ctx: CustomContext): void { 25 | console.log(`[${req.method}]-[${new URL(req.url).pathname}]-[${Date.now() - ctx.startTime}ms]`) 26 | } 27 | 28 | export async function html(req: Request, ctx: CustomContext): Promise { 29 | ctx.headers.set("Content-Type", "text/html") 30 | } 31 | -------------------------------------------------------------------------------- /src/handlers/handlers.tsx: -------------------------------------------------------------------------------- 1 | 2 | import type { CustomContext } from "../middleware/middleware"; 3 | import { BaseTemplate, HomeTemplate } from "../templates/templates"; 4 | import ReactDOMServer from 'react-dom/server'; 5 | 6 | 7 | export async function handleStatic(path: string): Promise { 8 | let file = await Bun.file("."+path) 9 | let exists = await file.exists() 10 | if (!exists) { 11 | return await notFound(); 12 | } 13 | return await new Response(file, { 14 | headers: { 15 | "Content-Type": file.type 16 | } 17 | 18 | }) 19 | } 20 | 21 | export async function handleFavicon(path: string): Promise { 22 | let file = await Bun.file("."+path) 23 | let exists = await file.exists() 24 | if (!exists) { 25 | return await notFound(); 26 | } 27 | return await new Response(file, { 28 | headers: { 29 | "Content-Type": file.type 30 | } 31 | 32 | }) 33 | } 34 | 35 | 36 | export async function handleHome(req: Request, ctx: CustomContext): Promise { 37 | const body = ReactDOMServer.renderToString(); 38 | return new Response(body, { 39 | headers: ctx.headers 40 | }); 41 | } 42 | 43 | 44 | export async function notFound(): Promise { 45 | return new Response("Not Found", { 46 | status: 404 47 | }); 48 | } -------------------------------------------------------------------------------- /static/css/output.css: -------------------------------------------------------------------------------- 1 | /* 2 | ! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com 3 | */ 4 | 5 | /* 6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4) 7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116) 8 | */ 9 | 10 | *, 11 | ::before, 12 | ::after { 13 | box-sizing: border-box; 14 | /* 1 */ 15 | border-width: 0; 16 | /* 2 */ 17 | border-style: solid; 18 | /* 2 */ 19 | border-color: #e5e7eb; 20 | /* 2 */ 21 | } 22 | 23 | ::before, 24 | ::after { 25 | --tw-content: ''; 26 | } 27 | 28 | /* 29 | 1. Use a consistent sensible line-height in all browsers. 30 | 2. Prevent adjustments of font size after orientation changes in iOS. 31 | 3. Use a more readable tab size. 32 | 4. Use the user's configured `sans` font-family by default. 33 | 5. Use the user's configured `sans` font-feature-settings by default. 34 | 6. Use the user's configured `sans` font-variation-settings by default. 35 | */ 36 | 37 | html { 38 | line-height: 1.5; 39 | /* 1 */ 40 | -webkit-text-size-adjust: 100%; 41 | /* 2 */ 42 | -moz-tab-size: 4; 43 | /* 3 */ 44 | -o-tab-size: 4; 45 | tab-size: 4; 46 | /* 3 */ 47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 48 | /* 4 */ 49 | font-feature-settings: normal; 50 | /* 5 */ 51 | font-variation-settings: normal; 52 | /* 6 */ 53 | } 54 | 55 | /* 56 | 1. Remove the margin in all browsers. 57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element. 58 | */ 59 | 60 | body { 61 | margin: 0; 62 | /* 1 */ 63 | line-height: inherit; 64 | /* 2 */ 65 | } 66 | 67 | /* 68 | 1. Add the correct height in Firefox. 69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655) 70 | 3. Ensure horizontal rules are visible by default. 71 | */ 72 | 73 | hr { 74 | height: 0; 75 | /* 1 */ 76 | color: inherit; 77 | /* 2 */ 78 | border-top-width: 1px; 79 | /* 3 */ 80 | } 81 | 82 | /* 83 | Add the correct text decoration in Chrome, Edge, and Safari. 84 | */ 85 | 86 | abbr:where([title]) { 87 | -webkit-text-decoration: underline dotted; 88 | text-decoration: underline dotted; 89 | } 90 | 91 | /* 92 | Remove the default font size and weight for headings. 93 | */ 94 | 95 | h1, 96 | h2, 97 | h3, 98 | h4, 99 | h5, 100 | h6 { 101 | font-size: inherit; 102 | font-weight: inherit; 103 | } 104 | 105 | /* 106 | Reset links to optimize for opt-in styling instead of opt-out. 107 | */ 108 | 109 | a { 110 | color: inherit; 111 | text-decoration: inherit; 112 | } 113 | 114 | /* 115 | Add the correct font weight in Edge and Safari. 116 | */ 117 | 118 | b, 119 | strong { 120 | font-weight: bolder; 121 | } 122 | 123 | /* 124 | 1. Use the user's configured `mono` font family by default. 125 | 2. Correct the odd `em` font sizing in all browsers. 126 | */ 127 | 128 | code, 129 | kbd, 130 | samp, 131 | pre { 132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 133 | /* 1 */ 134 | font-size: 1em; 135 | /* 2 */ 136 | } 137 | 138 | /* 139 | Add the correct font size in all browsers. 140 | */ 141 | 142 | small { 143 | font-size: 80%; 144 | } 145 | 146 | /* 147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers. 148 | */ 149 | 150 | sub, 151 | sup { 152 | font-size: 75%; 153 | line-height: 0; 154 | position: relative; 155 | vertical-align: baseline; 156 | } 157 | 158 | sub { 159 | bottom: -0.25em; 160 | } 161 | 162 | sup { 163 | top: -0.5em; 164 | } 165 | 166 | /* 167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297) 168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016) 169 | 3. Remove gaps between table borders by default. 170 | */ 171 | 172 | table { 173 | text-indent: 0; 174 | /* 1 */ 175 | border-color: inherit; 176 | /* 2 */ 177 | border-collapse: collapse; 178 | /* 3 */ 179 | } 180 | 181 | /* 182 | 1. Change the font styles in all browsers. 183 | 2. Remove the margin in Firefox and Safari. 184 | 3. Remove default padding in all browsers. 185 | */ 186 | 187 | button, 188 | input, 189 | optgroup, 190 | select, 191 | textarea { 192 | font-family: inherit; 193 | /* 1 */ 194 | font-feature-settings: inherit; 195 | /* 1 */ 196 | font-variation-settings: inherit; 197 | /* 1 */ 198 | font-size: 100%; 199 | /* 1 */ 200 | font-weight: inherit; 201 | /* 1 */ 202 | line-height: inherit; 203 | /* 1 */ 204 | color: inherit; 205 | /* 1 */ 206 | margin: 0; 207 | /* 2 */ 208 | padding: 0; 209 | /* 3 */ 210 | } 211 | 212 | /* 213 | Remove the inheritance of text transform in Edge and Firefox. 214 | */ 215 | 216 | button, 217 | select { 218 | text-transform: none; 219 | } 220 | 221 | /* 222 | 1. Correct the inability to style clickable types in iOS and Safari. 223 | 2. Remove default button styles. 224 | */ 225 | 226 | button, 227 | [type='button'], 228 | [type='reset'], 229 | [type='submit'] { 230 | -webkit-appearance: button; 231 | /* 1 */ 232 | background-color: transparent; 233 | /* 2 */ 234 | background-image: none; 235 | /* 2 */ 236 | } 237 | 238 | /* 239 | Use the modern Firefox focus style for all focusable elements. 240 | */ 241 | 242 | :-moz-focusring { 243 | outline: auto; 244 | } 245 | 246 | /* 247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737) 248 | */ 249 | 250 | :-moz-ui-invalid { 251 | box-shadow: none; 252 | } 253 | 254 | /* 255 | Add the correct vertical alignment in Chrome and Firefox. 256 | */ 257 | 258 | progress { 259 | vertical-align: baseline; 260 | } 261 | 262 | /* 263 | Correct the cursor style of increment and decrement buttons in Safari. 264 | */ 265 | 266 | ::-webkit-inner-spin-button, 267 | ::-webkit-outer-spin-button { 268 | height: auto; 269 | } 270 | 271 | /* 272 | 1. Correct the odd appearance in Chrome and Safari. 273 | 2. Correct the outline style in Safari. 274 | */ 275 | 276 | [type='search'] { 277 | -webkit-appearance: textfield; 278 | /* 1 */ 279 | outline-offset: -2px; 280 | /* 2 */ 281 | } 282 | 283 | /* 284 | Remove the inner padding in Chrome and Safari on macOS. 285 | */ 286 | 287 | ::-webkit-search-decoration { 288 | -webkit-appearance: none; 289 | } 290 | 291 | /* 292 | 1. Correct the inability to style clickable types in iOS and Safari. 293 | 2. Change font properties to `inherit` in Safari. 294 | */ 295 | 296 | ::-webkit-file-upload-button { 297 | -webkit-appearance: button; 298 | /* 1 */ 299 | font: inherit; 300 | /* 2 */ 301 | } 302 | 303 | /* 304 | Add the correct display in Chrome and Safari. 305 | */ 306 | 307 | summary { 308 | display: list-item; 309 | } 310 | 311 | /* 312 | Removes the default spacing and border for appropriate elements. 313 | */ 314 | 315 | blockquote, 316 | dl, 317 | dd, 318 | h1, 319 | h2, 320 | h3, 321 | h4, 322 | h5, 323 | h6, 324 | hr, 325 | figure, 326 | p, 327 | pre { 328 | margin: 0; 329 | } 330 | 331 | fieldset { 332 | margin: 0; 333 | padding: 0; 334 | } 335 | 336 | legend { 337 | padding: 0; 338 | } 339 | 340 | ol, 341 | ul, 342 | menu { 343 | list-style: none; 344 | margin: 0; 345 | padding: 0; 346 | } 347 | 348 | /* 349 | Reset default styling for dialogs. 350 | */ 351 | 352 | dialog { 353 | padding: 0; 354 | } 355 | 356 | /* 357 | Prevent resizing textareas horizontally by default. 358 | */ 359 | 360 | textarea { 361 | resize: vertical; 362 | } 363 | 364 | /* 365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300) 366 | 2. Set the default placeholder color to the user's configured gray 400 color. 367 | */ 368 | 369 | input::-moz-placeholder, textarea::-moz-placeholder { 370 | opacity: 1; 371 | /* 1 */ 372 | color: #9ca3af; 373 | /* 2 */ 374 | } 375 | 376 | input::placeholder, 377 | textarea::placeholder { 378 | opacity: 1; 379 | /* 1 */ 380 | color: #9ca3af; 381 | /* 2 */ 382 | } 383 | 384 | /* 385 | Set the default cursor for buttons. 386 | */ 387 | 388 | button, 389 | [role="button"] { 390 | cursor: pointer; 391 | } 392 | 393 | /* 394 | Make sure disabled buttons don't get the pointer cursor. 395 | */ 396 | 397 | :disabled { 398 | cursor: default; 399 | } 400 | 401 | /* 402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14) 403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210) 404 | This can trigger a poorly considered lint error in some tools but is included by design. 405 | */ 406 | 407 | img, 408 | svg, 409 | video, 410 | canvas, 411 | audio, 412 | iframe, 413 | embed, 414 | object { 415 | display: block; 416 | /* 1 */ 417 | vertical-align: middle; 418 | /* 2 */ 419 | } 420 | 421 | /* 422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14) 423 | */ 424 | 425 | img, 426 | video { 427 | max-width: 100%; 428 | height: auto; 429 | } 430 | 431 | /* Make elements with the HTML hidden attribute stay hidden by default */ 432 | 433 | [hidden] { 434 | display: none; 435 | } 436 | 437 | *, ::before, ::after { 438 | --tw-border-spacing-x: 0; 439 | --tw-border-spacing-y: 0; 440 | --tw-translate-x: 0; 441 | --tw-translate-y: 0; 442 | --tw-rotate: 0; 443 | --tw-skew-x: 0; 444 | --tw-skew-y: 0; 445 | --tw-scale-x: 1; 446 | --tw-scale-y: 1; 447 | --tw-pan-x: ; 448 | --tw-pan-y: ; 449 | --tw-pinch-zoom: ; 450 | --tw-scroll-snap-strictness: proximity; 451 | --tw-gradient-from-position: ; 452 | --tw-gradient-via-position: ; 453 | --tw-gradient-to-position: ; 454 | --tw-ordinal: ; 455 | --tw-slashed-zero: ; 456 | --tw-numeric-figure: ; 457 | --tw-numeric-spacing: ; 458 | --tw-numeric-fraction: ; 459 | --tw-ring-inset: ; 460 | --tw-ring-offset-width: 0px; 461 | --tw-ring-offset-color: #fff; 462 | --tw-ring-color: rgb(59 130 246 / 0.5); 463 | --tw-ring-offset-shadow: 0 0 #0000; 464 | --tw-ring-shadow: 0 0 #0000; 465 | --tw-shadow: 0 0 #0000; 466 | --tw-shadow-colored: 0 0 #0000; 467 | --tw-blur: ; 468 | --tw-brightness: ; 469 | --tw-contrast: ; 470 | --tw-grayscale: ; 471 | --tw-hue-rotate: ; 472 | --tw-invert: ; 473 | --tw-saturate: ; 474 | --tw-sepia: ; 475 | --tw-drop-shadow: ; 476 | --tw-backdrop-blur: ; 477 | --tw-backdrop-brightness: ; 478 | --tw-backdrop-contrast: ; 479 | --tw-backdrop-grayscale: ; 480 | --tw-backdrop-hue-rotate: ; 481 | --tw-backdrop-invert: ; 482 | --tw-backdrop-opacity: ; 483 | --tw-backdrop-saturate: ; 484 | --tw-backdrop-sepia: ; 485 | } 486 | 487 | ::backdrop { 488 | --tw-border-spacing-x: 0; 489 | --tw-border-spacing-y: 0; 490 | --tw-translate-x: 0; 491 | --tw-translate-y: 0; 492 | --tw-rotate: 0; 493 | --tw-skew-x: 0; 494 | --tw-skew-y: 0; 495 | --tw-scale-x: 1; 496 | --tw-scale-y: 1; 497 | --tw-pan-x: ; 498 | --tw-pan-y: ; 499 | --tw-pinch-zoom: ; 500 | --tw-scroll-snap-strictness: proximity; 501 | --tw-gradient-from-position: ; 502 | --tw-gradient-via-position: ; 503 | --tw-gradient-to-position: ; 504 | --tw-ordinal: ; 505 | --tw-slashed-zero: ; 506 | --tw-numeric-figure: ; 507 | --tw-numeric-spacing: ; 508 | --tw-numeric-fraction: ; 509 | --tw-ring-inset: ; 510 | --tw-ring-offset-width: 0px; 511 | --tw-ring-offset-color: #fff; 512 | --tw-ring-color: rgb(59 130 246 / 0.5); 513 | --tw-ring-offset-shadow: 0 0 #0000; 514 | --tw-ring-shadow: 0 0 #0000; 515 | --tw-shadow: 0 0 #0000; 516 | --tw-shadow-colored: 0 0 #0000; 517 | --tw-blur: ; 518 | --tw-brightness: ; 519 | --tw-contrast: ; 520 | --tw-grayscale: ; 521 | --tw-hue-rotate: ; 522 | --tw-invert: ; 523 | --tw-saturate: ; 524 | --tw-sepia: ; 525 | --tw-drop-shadow: ; 526 | --tw-backdrop-blur: ; 527 | --tw-backdrop-brightness: ; 528 | --tw-backdrop-contrast: ; 529 | --tw-backdrop-grayscale: ; 530 | --tw-backdrop-hue-rotate: ; 531 | --tw-backdrop-invert: ; 532 | --tw-backdrop-opacity: ; 533 | --tw-backdrop-saturate: ; 534 | --tw-backdrop-sepia: ; 535 | } 536 | 537 | .flex { 538 | display: flex; 539 | } 540 | 541 | .flex-col { 542 | flex-direction: column; 543 | } 544 | 545 | .border { 546 | border-width: 1px; 547 | } 548 | 549 | .border-b { 550 | border-bottom-width: 1px; 551 | } 552 | 553 | .p-4 { 554 | padding: 1rem; 555 | } 556 | 557 | .text-2xl { 558 | font-size: 1.5rem; 559 | line-height: 2rem; 560 | } --------------------------------------------------------------------------------