Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions week01/.gitignore
Original file line number Diff line number Diff line change
@@ -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
106 changes: 106 additions & 0 deletions week01/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@

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>`
- Use `bunx <package> <command>` instead of `npx <package> <command>`
- 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 { createRoot } from "react-dom/client";

// import .css files directly and it works
import './index.css';

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/**.mdx`.
160 changes: 160 additions & 0 deletions week01/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#!/usr/bin/env bun
import plugin from "bun-plugin-tailwind";
import { existsSync } from "fs";
import { rm } from "fs/promises";
import path from "path";

if (process.argv.includes("--help") || process.argv.includes("-h")) {
console.log(`
🏗️ Bun Build Script

Usage: bun run build.ts [options]

Common Options:
--outdir <path> Output directory (default: "dist")
--minify Enable minification (or --minify.whitespace, --minify.syntax, etc)
--sourcemap <type> Sourcemap type: none|linked|inline|external
--target <target> Build target: browser|bun|node
--format <format> Output format: esm|cjs|iife
--splitting Enable code splitting
--packages <type> Package handling: bundle|external
--public-path <path> Public path for assets
--env <mode> Environment handling: inline|disable|prefix*
--conditions <list> Package.json export conditions (comma separated)
--external <list> External packages (comma separated)
--banner <text> Add banner text to output
--footer <text> Add footer text to output
--define <obj> Define global constants (e.g. --define.VERSION=1.0.0)
--help, -h Show this help message

Example:
bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom
`);
process.exit(0);
}

const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, (_, letter: string) => letter.toUpperCase());

const parseValue = (value: string): any => {
if (value === "true") return true;
if (value === "false") return false;

if (/^\d+$/.test(value)) return parseInt(value, 10);
if (/^\d*\.\d+$/.test(value)) return parseFloat(value);

if (value.includes(",")) return value.split(",").map(v => v.trim());

return value;
};

function parseArgs(): Partial<Bun.BuildConfig> {
const config: Record<string, unknown> = {};
const args = process.argv.slice(2);

for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === undefined) continue;
if (!arg.startsWith("--")) continue;

if (arg.startsWith("--no-")) {
const key = toCamelCase(arg.slice(5));
config[key] = false;
continue;
}

if (!arg.includes("=") && (i === args.length - 1 || args[i + 1]?.startsWith("--"))) {
const key = toCamelCase(arg.slice(2));
config[key] = true;
continue;
}

let key: string;
let value: string;

if (arg.includes("=")) {
[key, value] = arg.slice(2).split("=", 2) as [string, string];
} else {
key = arg.slice(2);
value = args[++i] ?? "";
}

key = toCamelCase(key);

if (key.includes(".")) {
const parts = key.split(".");
if (parts.length > 2) {
console.warn(
`Warning: Deeply nested option "${key}" is not supported. Only single-level nesting (e.g., --minify.whitespace) is allowed.`,
);
continue;
}
const parentKey = parts[0]!;
const childKey = parts[1]!;
const existing = config[parentKey];
if (typeof existing !== "object" || existing === null || Array.isArray(existing)) {
config[parentKey] = {};
}
(config[parentKey] as Record<string, unknown>)[childKey] = parseValue(value);
} else {
config[key] = parseValue(value);
}
}

return config as Partial<Bun.BuildConfig>;
}

const formatFileSize = (bytes: number): string => {
const units = ["B", "KB", "MB", "GB"];
let size = bytes;
let unitIndex = 0;

while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}

return `${size.toFixed(2)} ${units[unitIndex]}`;
};

console.log("\n🚀 Starting build process...\n");

const cliConfig = parseArgs();
const outdir = cliConfig.outdir || path.join(process.cwd(), "dist");

if (existsSync(outdir)) {
console.log(`🗑️ Cleaning previous build at ${outdir}`);
await rm(outdir, { recursive: true, force: true });
}

const start = performance.now();

const entrypoints = [...new Bun.Glob("**.html").scanSync("src")]
.map(a => path.resolve("src", a))
.filter(dir => !dir.includes("node_modules"));
console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`);

const result = await Bun.build({
entrypoints,
outdir,
plugins: [plugin],
minify: true,
target: "browser",
sourcemap: "linked",
define: {
"process.env.NODE_ENV": JSON.stringify("production"),
},
...cliConfig,
});

const end = performance.now();

const outputTable = result.outputs.map(output => ({
File: path.relative(process.cwd(), output.path),
Type: output.kind,
Size: formatFileSize(output.size),
}));

console.table(outputTable);
const buildTime = (end - start).toFixed(2);

console.log(`\n✅ Build completed in ${buildTime}ms\n`);
17 changes: 17 additions & 0 deletions week01/bun-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Generated by `bun init`

declare module "*.svg" {
/**
* A path to the SVG file
*/
const path: `${string}.svg`;
export = path;
}

declare module "*.module.css" {
/**
* A record of class names to their corresponding CSS module classes
*/
const classes: { readonly [key: string]: string };
export = classes;
}
71 changes: 71 additions & 0 deletions week01/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions week01/bunfig.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

[serve.static]
plugins = ["bun-plugin-tailwind"]
env = "BUN_PUBLIC_*"
Loading