diff --git a/.claude/hooks/post-edit-zig-format.js b/.claude/hooks/post-edit-zig-format.js new file mode 100755 index 0000000000..e70d5eb99e --- /dev/null +++ b/.claude/hooks/post-edit-zig-format.js @@ -0,0 +1,88 @@ +#!/usr/bin/env bun +import { extname } from "path"; +import { spawnSync } from "child_process"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const filePath = toolInput.file_path; + +// Only process Write, Edit, and MultiEdit tools +if (!["Write", "Edit", "MultiEdit"].includes(toolName)) { + process.exit(0); +} + +const ext = extname(filePath); + +// Only format known files +if (!filePath) { + process.exit(0); +} + +function formatZigFile() { + try { + // Format the Zig file + const result = spawnSync("vendor/zig/zig.exe", ["fmt", filePath], { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }); + + if (result.error) { + console.error(`Failed to format ${filePath}: ${result.error.message}`); + process.exit(0); + } + + if (result.status !== 0) { + console.error(`zig fmt failed for ${filePath}:`); + if (result.stderr) { + console.error(result.stderr); + } + process.exit(0); + } + } catch (error) {} +} + +function formatTypeScriptFile() { + try { + // Format the TypeScript file + const result = spawnSync( + "./node_modules/.bin/prettier", + ["--plugin=prettier-plugin-organize-imports", "--config", ".prettierrc", "--write", filePath], + { + cwd: process.env.CLAUDE_PROJECT_DIR || process.cwd(), + encoding: "utf-8", + }, + ); + } catch (error) {} +} + +if (ext === ".zig") { + formatZigFile(); +} else if ( + [ + ".cjs", + ".css", + ".html", + ".js", + ".json", + ".jsonc", + ".jsx", + ".less", + ".mjs", + ".pcss", + ".postcss", + ".sass", + ".scss", + ".styl", + ".stylus", + ".toml", + ".ts", + ".tsx", + ".yaml", + ].includes(ext) +) { + formatTypeScriptFile(); +} + +process.exit(0); diff --git a/.claude/hooks/pre-bash-zig-build.js b/.claude/hooks/pre-bash-zig-build.js new file mode 100755 index 0000000000..d65b8b43d7 --- /dev/null +++ b/.claude/hooks/pre-bash-zig-build.js @@ -0,0 +1,207 @@ +#!/usr/bin/env bun +import { basename, extname } from "path"; + +const input = await Bun.stdin.json(); + +const toolName = input.tool_name; +const toolInput = input.tool_input || {}; +const command = toolInput.command || ""; +const timeout = toolInput.timeout; +const cwd = input.cwd || ""; + +// Get environment variables from the hook context +// Note: We check process.env directly as env vars are inherited +let useSystemBun = process.env.USE_SYSTEM_BUN; + +if (toolName !== "Bash" || !command) { + process.exit(0); +} + +function denyWithReason(reason) { + const output = { + hookSpecificOutput: { + hookEventName: "PreToolUse", + permissionDecision: "deny", + permissionDecisionReason: reason, + }, + }; + console.log(JSON.stringify(output)); + process.exit(0); +} + +// Parse the command to extract argv0 and positional args +let tokens; +try { + // Simple shell parsing - split on spaces but respect quotes (both single and double) + tokens = command.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map(t => t.replace(/^['"]|['"]$/g, "")) || []; +} catch { + process.exit(0); +} + +if (tokens.length === 0) { + process.exit(0); +} + +// Strip inline environment variable assignments (e.g., FOO=1 bun test) +const inlineEnv = new Map(); +let commandStart = 0; +while ( + commandStart < tokens.length && + /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[commandStart]) && + !tokens[commandStart].includes("/") +) { + const [name, value = ""] = tokens[commandStart].split("=", 2); + inlineEnv.set(name, value); + commandStart++; +} +if (commandStart >= tokens.length) { + process.exit(0); +} +tokens = tokens.slice(commandStart); +useSystemBun = inlineEnv.get("USE_SYSTEM_BUN") ?? useSystemBun; + +// Get the executable name (argv0) +const argv0 = basename(tokens[0], extname(tokens[0])); + +// Check if it's zig or zig.exe +if (argv0 === "zig") { + // Filter out flags (starting with -) to get positional arguments + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + + // Check if the positional args contain "build" followed by "obj" + if (positionalArgs.length >= 2 && positionalArgs[0] === "build" && positionalArgs[1] === "obj") { + denyWithReason("error: Use `bun bd` to build Bun and wait patiently"); + } +} + +// Check if argv0 is timeout and the command is "bun bd" +if (argv0 === "timeout") { + // Find the actual command after timeout and its arguments + const timeoutArgEndIndex = tokens.slice(1).findIndex(t => !t.startsWith("-") && !/^\d/.test(t)); + if (timeoutArgEndIndex === -1) { + process.exit(0); + } + + const actualCommandIndex = timeoutArgEndIndex + 1; + if (actualCommandIndex >= tokens.length) { + process.exit(0); + } + + const actualCommand = basename(tokens[actualCommandIndex]); + const restArgs = tokens.slice(actualCommandIndex + 1); + + // Check if it's "bun bd" or "bun-debug bd" without other positional args + if (actualCommand === "bun" || actualCommand.includes("bun-debug")) { + // Claude is a sneaky fucker + let positionalArgs = restArgs.filter(arg => !arg.startsWith("-")); + const redirectStderrToStdoutIndex = positionalArgs.findIndex(arg => arg === "2>&1"); + if (redirectStderrToStdoutIndex !== -1) { + positionalArgs.splice(redirectStderrToStdoutIndex, 1); + } + const redirectStdoutToStderrIndex = positionalArgs.findIndex(arg => arg === "1>&2"); + if (redirectStdoutToStderrIndex !== -1) { + positionalArgs.splice(redirectStdoutToStderrIndex, 1); + } + + const redirectToFileIndex = positionalArgs.findIndex(arg => arg === ">"); + if (redirectToFileIndex !== -1) { + positionalArgs.splice(redirectToFileIndex, 2); + } + + const redirectToFileAppendIndex = positionalArgs.findIndex(arg => arg === ">>"); + if (redirectToFileAppendIndex !== -1) { + positionalArgs.splice(redirectToFileAppendIndex, 2); + } + + const redirectTOFileInlineIndex = positionalArgs.findIndex(arg => arg.startsWith(">")); + if (redirectTOFileInlineIndex !== -1) { + positionalArgs.splice(redirectTOFileInlineIndex, 1); + } + + const pipeIndex = positionalArgs.findIndex(arg => arg === "|"); + if (pipeIndex !== -1) { + positionalArgs = positionalArgs.slice(0, pipeIndex); + } + + positionalArgs = positionalArgs.map(arg => arg.trim()).filter(Boolean); + + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } + } +} + +// Check if command is "bun .* test" or "bun-debug test" with -u/--update-snapshots AND -t/--test-name-pattern +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + + // Check if "test" is in positional args or "bd" followed by "test" + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + const hasTest = positionalArgs.includes("test") || (positionalArgs[0] === "bd" && positionalArgs[1] === "test"); + + if (hasTest) { + const hasUpdateSnapshots = allArgs.some(arg => arg === "-u" || arg === "--update-snapshots"); + const hasTestNamePattern = allArgs.some(arg => arg === "-t" || arg === "--test-name-pattern"); + + if (hasUpdateSnapshots && hasTestNamePattern) { + denyWithReason("error: Cannot use -u/--update-snapshots with -t/--test-name-pattern"); + } + } +} + +// Check if timeout option is set for "bun bd" command +if (timeout !== undefined && (argv0 === "bun" || argv0.includes("bun-debug"))) { + const positionalArgs = tokens.slice(1).filter(arg => !arg.startsWith("-")); + if (positionalArgs.length === 1 && positionalArgs[0] === "bd") { + denyWithReason("error: Run `bun bd` without a timeout"); + } +} + +// Check if running "bun test " without USE_SYSTEM_BUN=1 +if ((argv0 === "bun" || argv0.includes("bun-debug")) && useSystemBun !== "1") { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "test" (not "bd test") + if (positionalArgs.length >= 1 && positionalArgs[0] === "test" && positionalArgs[0] !== "bd") { + denyWithReason( + "error: In development, use `bun bd test ` to test your changes. If you meant to use a release version, set USE_SYSTEM_BUN=1", + ); + } +} + +// Check if running "bun bd test" from bun repo root or test folder without a file path +if (argv0 === "bun" || argv0.includes("bun-debug")) { + const allArgs = tokens.slice(1); + const positionalArgs = allArgs.filter(arg => !arg.startsWith("-")); + + // Check if it's "bd test" + if (positionalArgs.length >= 2 && positionalArgs[0] === "bd" && positionalArgs[1] === "test") { + // Check if cwd is the bun repo root or test folder + const isBunRepoRoot = cwd === "/workspace/bun" || cwd.endsWith("/bun"); + const isTestFolder = cwd.endsWith("/bun/test"); + + if (isBunRepoRoot || isTestFolder) { + // Check if there's a file path argument (looks like a path: contains / or has test extension) + const hasFilePath = positionalArgs + .slice(2) + .some( + arg => + arg.includes("/") || + arg.endsWith(".test.ts") || + arg.endsWith(".test.js") || + arg.endsWith(".test.tsx") || + arg.endsWith(".test.jsx"), + ); + + if (!hasFilePath) { + denyWithReason( + "error: `bun bd test` from repo root or test folder will run all tests. Use `bun bd test ` with a specific test file.", + ); + } + } + } +} + +// Allow the command to proceed +process.exit(0); diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000000..387633d5fd --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "PreToolUse": [ + { + "matcher": "Bash", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/pre-bash-zig-build.js" + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/post-edit-zig-format.js" + } + ] + } + ] + } +} diff --git a/.cursor/rules/building-bun.mdc b/.cursor/rules/building-bun.mdc index 0a5fa27f2c..2fef59b551 100644 --- a/.cursor/rules/building-bun.mdc +++ b/.cursor/rules/building-bun.mdc @@ -30,7 +30,7 @@ bun bd <...args> Debug logs look like this: ```zig -const log = bun.Output.scoped(.${SCOPE}, false); +const log = bun.Output.scoped(.${SCOPE}, .hidden); // ...later log("MY DEBUG LOG", .{}) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 77b5551f9d..3ab51a4309 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -57,8 +57,7 @@ jobs: git reset --hard origin/${{ github.event.pull_request.head.ref }} - name: Run Claude Code id: claude - # TODO: switch this out once they merge their v1 - uses: km-anthropic/claude-code-action@v1-dev + uses: anthropics/claude-code-action@v1 with: timeout_minutes: "180" claude_args: | diff --git a/.github/workflows/labeled.yml b/.github/workflows/labeled.yml index 8b965d2f07..08c09c3741 100644 --- a/.github/workflows/labeled.yml +++ b/.github/workflows/labeled.yml @@ -142,8 +142,8 @@ jobs: uses: actions/github-script@v7 with: script: | - const closeAction = JSON.parse('${{ steps.add-labels.outputs.close-action }}'); - + const closeAction = ${{ fromJson(steps.add-labels.outputs.close-action) }}; + // Comment with the reason await github.rest.issues.createComment({ owner: context.repo.owner, @@ -151,7 +151,7 @@ jobs: issue_number: context.issue.number, body: closeAction.comment }); - + // Close the issue await github.rest.issues.update({ owner: context.repo.owner, diff --git a/.github/workflows/update-sqlite3.yml b/.github/workflows/update-sqlite3.yml index 6ee8115f7c..65321f466a 100644 --- a/.github/workflows/update-sqlite3.yml +++ b/.github/workflows/update-sqlite3.yml @@ -70,24 +70,7 @@ jobs: - name: Update SQLite if needed if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num run: | - set -euo pipefail - - TEMP_DIR=$(mktemp -d) - cd $TEMP_DIR - - echo "Downloading from: https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - - # Download and extract latest version - wget "https://sqlite.org/${{ steps.check-version.outputs.latest_year }}/sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - unzip "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}.zip" - cd "sqlite-amalgamation-${{ steps.check-version.outputs.latest_num }}" - - # Add header comment and copy files - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - cat sqlite3.c >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3.c - - echo "// clang-format off" > $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h - cat sqlite3.h >> $GITHUB_WORKSPACE/src/bun.js/bindings/sqlite/sqlite3_local.h + ./scripts/update-sqlite-amalgamation.sh ${{ steps.check-version.outputs.latest_num }} ${{ steps.check-version.outputs.latest_year }} - name: Create Pull Request if: success() && steps.check-version.outputs.current_num < steps.check-version.outputs.latest_num diff --git a/.gitignore b/.gitignore index 3f71c2acc9..4b95245f9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ +.claude/settings.local.json .DS_Store .env .envrc .eslintcache +.gdb_history .idea .next .ninja_deps @@ -189,4 +191,4 @@ scratch*.{js,ts,tsx,cjs,mjs} scripts/lldb-inline # We regenerate these in all the build scripts -cmake/sources/*.txt \ No newline at end of file +cmake/sources/*.txt diff --git a/.prettierrc b/.prettierrc index c9da1bd439..14285ca704 100644 --- a/.prettierrc +++ b/.prettierrc @@ -19,6 +19,12 @@ "options": { "printWidth": 80 } + }, + { + "files": ["src/codegen/bindgenv2/**/*.ts", "*.bindv2.ts"], + "options": { + "printWidth": 100 + } } ] } diff --git a/CLAUDE.md b/CLAUDE.md index 09a8499345..5fa59d403c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,12 +23,15 @@ Tip: Bun is already installed and in $PATH. The `bd` subcommand is a package.jso ### Test Organization +If a test is for a specific numbered GitHub Issue, it should be placed in `test/regression/issue/${issueNumber}.test.ts`. Ensure the issue number is **REAL** and not a placeholder! + +If no valid issue number is provided, find the best existing file to modify instead, such as; + - `test/js/bun/` - Bun-specific API tests (http, crypto, ffi, shell, etc.) - `test/js/node/` - Node.js compatibility tests - `test/js/web/` - Web API tests (fetch, WebSocket, streams, etc.) - `test/cli/` - CLI command tests (install, run, test, etc.) -- `test/regression/issue/` - Regression tests (create one per bug fix) -- `test/bundler/` - Bundler and transpiler tests +- `test/bundler/` - Bundler and transpiler tests. Use `itBundled` helper. - `test/integration/` - End-to-end integration tests - `test/napi/` - N-API compatibility tests - `test/v8/` - V8 C++ API compatibility tests @@ -61,15 +64,20 @@ test("my feature", async () => { proc.exited, ]); - expect(exitCode).toBe(0); // Prefer snapshot tests over expect(stdout).toBe("hello\n"); expect(normalizeBunSnapshot(stdout, dir)).toMatchInlineSnapshot(`"hello"`); + + // Assert the exit code last. This gives you a more useful error message on test failure. + expect(exitCode).toBe(0); }); ``` - Always use `port: 0`. Do not hardcode ports. Do not use your own random port number function. - Use `normalizeBunSnapshot` to normalize snapshot output of the test. - NEVER write tests that check for no "panic" or "uncaught exception" or similar in the test output. That is NOT a valid test. +- Use `tempDir` from `"harness"` to create a temporary directory. **Do not** use `tmpdirSync` or `fs.mkdtempSync` to create temporary directories. +- When spawning processes, tests should assert the output BEFORE asserting the exit code. This gives you a more useful error message on test failure. +- **CRITICAL**: Verify your test fails with `USE_SYSTEM_BUN=1 bun test ` and passes with `bun bd test `. Your test is NOT VALID if it passes with `USE_SYSTEM_BUN=1`. ## Code Architecture @@ -78,7 +86,7 @@ test("my feature", async () => { - **Zig code** (`src/*.zig`): Core runtime, JavaScript bindings, package manager - **C++ code** (`src/bun.js/bindings/*.cpp`): JavaScriptCore bindings, Web APIs - **TypeScript** (`src/js/`): Built-in JavaScript modules with special syntax (see JavaScript Modules section) -- **Generated code**: Many files are auto-generated from `.classes.ts` and other sources +- **Generated code**: Many files are auto-generated from `.classes.ts` and other sources. Bun will automatically rebuild these files when you make changes to them. ### Core Source Organization @@ -143,19 +151,6 @@ When implementing JavaScript classes in C++: 3. Add iso subspaces for classes with C++ fields 4. Cache structures in ZigGlobalObject -## Development Workflow - -### Code Formatting - -- `bun run prettier` - Format JS/TS files -- `bun run zig-format` - Format Zig files -- `bun run clang-format` - Format C++ files - -### Watching for Changes - -- `bun run watch` - Incremental Zig compilation with error checking -- `bun run watch-windows` - Windows-specific watch mode - ### Code Generation Code generation happens automatically as part of the build process. The main scripts are: @@ -177,47 +172,6 @@ Built-in JavaScript modules use special syntax and are organized as: - `internal/` - Internal modules not exposed to users - `builtins/` - Core JavaScript builtins (streams, console, etc.) -### Special Syntax in Built-in Modules - -1. **`$` prefix** - Access to private properties and JSC intrinsics: - - ```js - const arr = $Array.from(...); // Private global - map.$set(...); // Private method - const arr2 = $newArrayWithSize(5); // JSC intrinsic - ``` - -2. **`require()`** - Must use string literals, resolved at compile time: - - ```js - const fs = require("fs"); // Directly loads by numeric ID - ``` - -3. **Debug helpers**: - - `$debug()` - Like console.log but stripped in release builds - - `$assert()` - Assertions stripped in release builds - - `if($debug) {}` - Check if debug env var is set - -4. **Platform detection**: `process.platform` and `process.arch` are inlined and dead-code eliminated - -5. **Export syntax**: Use `export default` which gets converted to a return statement: - ```js - export default { - readFile, - writeFile, - }; - ``` - -Note: These are NOT ES modules. The preprocessor converts `$` to `@` (JSC's actual syntax) and handles the special functions. - -## CI - -Bun uses BuildKite for CI. To get the status of a PR, you can use the following command: - -```bash -bun ci -``` - ## Important Development Notes 1. **Never use `bun test` or `bun ` directly** - always use `bun bd test` or `bun bd `. `bun bd` compiles & runs the debug build. @@ -229,19 +183,8 @@ bun ci 7. **Avoid shell commands** - Don't use `find` or `grep` in tests; use Bun's Glob and built-in tools 8. **Memory management** - In Zig code, be careful with allocators and use defer for cleanup 9. **Cross-platform** - Run `bun run zig:check-all` to compile the Zig code on all platforms when making platform-specific changes -10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific scopes +10. **Debug builds** - Use `BUN_DEBUG_QUIET_LOGS=1` to disable debug logging, or `BUN_DEBUG_=1` to enable specific `Output.scoped(.${scopeName}, .visible)`s 11. **Be humble & honest** - NEVER overstate what you got done or what actually works in commits, PRs or in messages to the user. 12. **Branch names must start with `claude/`** - This is a requirement for the CI to work. -## Key APIs and Features - -### Bun-Specific APIs - -- **Bun.serve()** - High-performance HTTP server -- **Bun.spawn()** - Process spawning with better performance than Node.js -- **Bun.file()** - Fast file I/O operations -- **Bun.write()** - Unified API for writing to files, stdout, etc. -- **Bun.$ (Shell)** - Cross-platform shell scripting -- **Bun.SQLite** - Native SQLite integration -- **Bun.FFI** - Call native libraries from JavaScript -- **Bun.Glob** - Fast file pattern matching +**ONLY** push up changes after running `bun bd test ` and ensuring your tests pass. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c39fd4463a..487e0d4888 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,21 @@ Configuring a development environment for Bun can take 10-30 minutes depending o If you are using Windows, please refer to [this guide](https://bun.com/docs/project/building-windows) -## Install Dependencies +## Using Nix (Alternative) + +A Nix flake is provided as an alternative to manual dependency installation: + +```bash +nix develop +# or explicitly use the pure shell +# nix develop .#pure +export CMAKE_SYSTEM_PROCESSOR=$(uname -m) +bun bd +``` + +This provides all dependencies in an isolated, reproducible environment without requiring sudo. + +## Install Dependencies (Manual) Using your system's package manager, install Bun's dependencies: @@ -149,7 +163,7 @@ Bun generally takes about 2.5 minutes to compile a debug build when there are Zi - Batch up your changes - Ensure zls is running with incremental watching for LSP errors (if you use VSCode and install Zig and run `bun run build` once to download Zig, this should just work) - Prefer using the debugger ("CodeLLDB" in VSCode) to step through the code. -- Use debug logs. `BUN_DEBUG_=1` will enable debug logging for the corresponding `Output.scoped(., false)` logs. You can also set `BUN_DEBUG_QUIET_LOGS=1` to disable all debug logging that isn't explicitly enabled. To dump debug lgos into a file, `BUN_DEBUG=.log`. Debug logs are aggressively removed in release builds. +- Use debug logs. `BUN_DEBUG_=1` will enable debug logging for the corresponding `Output.scoped(., .hidden)` logs. You can also set `BUN_DEBUG_QUIET_LOGS=1` to disable all debug logging that isn't explicitly enabled. To dump debug lgos into a file, `BUN_DEBUG=.log`. Debug logs are aggressively removed in release builds. - src/js/\*\*.ts changes are pretty much instant to rebuild. C++ changes are a bit slower, but still much faster than the Zig code (Zig is one compilation unit, C++ is many). ## Code generation scripts diff --git a/LATEST b/LATEST index a93a6f7571..f0bb29e763 100644 --- a/LATEST +++ b/LATEST @@ -1 +1 @@ -1.2.23 +1.3.0 diff --git a/build.zig b/build.zig index fa76a2138c..0f45dce6e1 100644 --- a/build.zig +++ b/build.zig @@ -49,6 +49,7 @@ const BunBuildOptions = struct { enable_logs: bool = false, enable_asan: bool, enable_valgrind: bool, + use_mimalloc: bool, tracy_callstack_depth: u16, reported_nodejs_version: Version, /// To make iterating on some '@embedFile's faster, we load them at runtime @@ -97,6 +98,7 @@ const BunBuildOptions = struct { opts.addOption(bool, "enable_logs", this.enable_logs); opts.addOption(bool, "enable_asan", this.enable_asan); opts.addOption(bool, "enable_valgrind", this.enable_valgrind); + opts.addOption(bool, "use_mimalloc", this.use_mimalloc); opts.addOption([]const u8, "reported_nodejs_version", b.fmt("{}", .{this.reported_nodejs_version})); opts.addOption(bool, "zig_self_hosted_backend", this.no_llvm); opts.addOption(bool, "override_no_export_cpp_apis", this.override_no_export_cpp_apis); @@ -270,6 +272,7 @@ pub fn build(b: *Build) !void { .enable_logs = b.option(bool, "enable_logs", "Enable logs in release") orelse false, .enable_asan = b.option(bool, "enable_asan", "Enable asan") orelse false, .enable_valgrind = b.option(bool, "enable_valgrind", "Enable valgrind") orelse false, + .use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false, .llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1, }; @@ -500,6 +503,7 @@ fn addMultiCheck( .no_llvm = root_build_options.no_llvm, .enable_asan = root_build_options.enable_asan, .enable_valgrind = root_build_options.enable_valgrind, + .use_mimalloc = root_build_options.use_mimalloc, .override_no_export_cpp_apis = root_build_options.override_no_export_cpp_apis, }; @@ -720,6 +724,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { // Generated code exposed as individual modules. inline for (.{ .{ .file = "ZigGeneratedClasses.zig", .import = "ZigGeneratedClasses" }, + .{ .file = "bindgen_generated.zig", .import = "bindgen_generated" }, .{ .file = "ResolvedSourceTag.zig", .import = "ResolvedSourceTag" }, .{ .file = "ErrorCode.zig", .import = "ErrorCode" }, .{ .file = "runtime.out.js", .enable = opts.shouldEmbedCode() }, diff --git a/bun.lock b/bun.lock index be4ab107ae..fedf606d75 100644 --- a/bun.lock +++ b/bun.lock @@ -8,14 +8,14 @@ "@lezer/cpp": "^1.1.3", "@types/bun": "workspace:*", "bun-tracestrings": "github:oven-sh/bun.report#912ca63e26c51429d3e6799aa2a6ab079b188fd8", - "esbuild": "^0.21.4", - "mitata": "^0.1.11", + "esbuild": "^0.21.5", + "mitata": "^0.1.14", "peechy": "0.4.34", - "prettier": "^3.5.3", - "prettier-plugin-organize-imports": "^4.0.0", + "prettier": "^3.6.2", + "prettier-plugin-organize-imports": "^4.3.0", "react": "^18.3.1", "react-dom": "^18.3.1", - "source-map-js": "^1.2.0", + "source-map-js": "^1.2.1", "typescript": "5.9.2", }, }, @@ -284,7 +284,7 @@ "prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="], - "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.2.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-Zdy27UhlmyvATZi67BTnLcKTo8fm6Oik59Sz6H64PgZJVs6NJpPD1mT240mmJn62c98/QaL+r3kx9Q3gRpDajg=="], + "prettier-plugin-organize-imports": ["prettier-plugin-organize-imports@4.3.0", "", { "peerDependencies": { "prettier": ">=2.0", "typescript": ">=2.9", "vue-tsc": "^2.1.0 || 3" }, "optionalPeers": ["vue-tsc"] }, "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw=="], "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], diff --git a/bunfig.toml b/bunfig.toml index 3eae059d7c..f1bba3259c 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -10,3 +10,4 @@ preload = "./test/preload.ts" [install] linker = "isolated" +minimumReleaseAge = 1 diff --git a/cmake/CompilerFlags.cmake b/cmake/CompilerFlags.cmake index 9f97e31dc1..b9ae804f06 100644 --- a/cmake/CompilerFlags.cmake +++ b/cmake/CompilerFlags.cmake @@ -86,11 +86,20 @@ elseif(APPLE) endif() if(UNIX) - register_compiler_flags( - DESCRIPTION "Enable debug symbols" - -g3 -gz=zstd ${DEBUG} - -g1 ${RELEASE} - ) + # Nix LLVM doesn't support zstd compression, use zlib instead + if(DEFINED ENV{NIX_CC}) + register_compiler_flags( + DESCRIPTION "Enable debug symbols (zlib-compressed for Nix)" + -g3 -gz=zlib ${DEBUG} + -g1 ${RELEASE} + ) + else() + register_compiler_flags( + DESCRIPTION "Enable debug symbols (zstd-compressed)" + -g3 -gz=zstd ${DEBUG} + -g1 ${RELEASE} + ) + endif() register_compiler_flags( DESCRIPTION "Optimize debug symbols for LLDB" @@ -214,10 +223,13 @@ if(ENABLE_ASSERTIONS) _LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_DEBUG ${DEBUG} ) - register_compiler_definitions( - DESCRIPTION "Enable fortified sources" - _FORTIFY_SOURCE=3 - ) + # Nix glibc already sets _FORTIFY_SOURCE, don't override it + if(NOT DEFINED ENV{NIX_CC}) + register_compiler_definitions( + DESCRIPTION "Enable fortified sources (Release only)" + _FORTIFY_SOURCE=3 ${RELEASE} + ) + endif() if(LINUX) register_compiler_definitions( diff --git a/cmake/Options.cmake b/cmake/Options.cmake index 1e9b664321..93a3698563 100644 --- a/cmake/Options.cmake +++ b/cmake/Options.cmake @@ -202,4 +202,9 @@ optionx(USE_WEBKIT_ICU BOOL "Use the ICU libraries from WebKit" DEFAULT ${DEFAUL optionx(ERROR_LIMIT STRING "Maximum number of errors to show when compiling C++ code" DEFAULT "100") +# This is not an `option` because setting this variable to OFF is experimental +# and unsupported. This replaces the `use_mimalloc` variable previously in +# bun.zig, and enables C++ code to also be aware of the option. +set(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR ON) + list(APPEND CMAKE_ARGS -DCMAKE_EXPORT_COMPILE_COMMANDS=ON) diff --git a/cmake/Sources.json b/cmake/Sources.json index cd86d86989..5ae4930693 100644 --- a/cmake/Sources.json +++ b/cmake/Sources.json @@ -31,6 +31,14 @@ "output": "BindgenSources.txt", "paths": ["src/**/*.bind.ts"] }, + { + "output": "BindgenV2Sources.txt", + "paths": ["src/**/*.bindv2.ts"] + }, + { + "output": "BindgenV2InternalSources.txt", + "paths": ["src/codegen/bindgenv2/**/*.ts"] + }, { "output": "ZigSources.txt", "paths": ["src/**/*.zig"] diff --git a/cmake/targets/BuildBun.cmake b/cmake/targets/BuildBun.cmake index 31b007050c..c31c8a4de5 100644 --- a/cmake/targets/BuildBun.cmake +++ b/cmake/targets/BuildBun.cmake @@ -395,6 +395,54 @@ register_command( ${BUN_BAKE_RUNTIME_OUTPUTS} ) +set(BUN_BINDGENV2_SCRIPT ${CWD}/src/codegen/bindgenv2/script.ts) + +absolute_sources(BUN_BINDGENV2_SOURCES ${CWD}/cmake/sources/BindgenV2Sources.txt) +# These sources include the script itself. +absolute_sources(BUN_BINDGENV2_INTERNAL_SOURCES + ${CWD}/cmake/sources/BindgenV2InternalSources.txt) +string(REPLACE ";" "," BUN_BINDGENV2_SOURCES_COMMA_SEPARATED + "${BUN_BINDGENV2_SOURCES}") + +execute_process( + COMMAND ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=list-outputs + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + --codegen-path=${CODEGEN_PATH} + RESULT_VARIABLE bindgen_result + OUTPUT_VARIABLE bindgen_outputs +) +if(${bindgen_result}) + message(FATAL_ERROR "bindgenv2/script.ts exited with non-zero status") +endif() +foreach(output IN LISTS bindgen_outputs) + if(output MATCHES "\.cpp$") + list(APPEND BUN_BINDGENV2_CPP_OUTPUTS ${output}) + elseif(output MATCHES "\.zig$") + list(APPEND BUN_BINDGENV2_ZIG_OUTPUTS ${output}) + else() + message(FATAL_ERROR "unexpected bindgen output: [${output}]") + endif() +endforeach() + +register_command( + TARGET + bun-bindgen-v2 + COMMENT + "Generating bindings (v2)" + COMMAND + ${BUN_EXECUTABLE} run ${BUN_BINDGENV2_SCRIPT} + --command=generate + --codegen-path=${CODEGEN_PATH} + --sources=${BUN_BINDGENV2_SOURCES_COMMA_SEPARATED} + SOURCES + ${BUN_BINDGENV2_SOURCES} + ${BUN_BINDGENV2_INTERNAL_SOURCES} + OUTPUTS + ${BUN_BINDGENV2_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} +) + set(BUN_BINDGEN_SCRIPT ${CWD}/src/codegen/bindgen.ts) absolute_sources(BUN_BINDGEN_SOURCES ${CWD}/cmake/sources/BindgenSources.txt) @@ -573,6 +621,7 @@ set(BUN_ZIG_GENERATED_SOURCES ${BUN_ZIG_GENERATED_CLASSES_OUTPUTS} ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_CPP_OUTPUTS} + ${BUN_BINDGENV2_ZIG_OUTPUTS} ) # In debug builds, these are not embedded, but rather referenced at runtime. @@ -636,6 +685,7 @@ register_command( -Denable_logs=$,true,false> -Denable_asan=$,true,false> -Denable_valgrind=$,true,false> + -Duse_mimalloc=$,true,false> -Dllvm_codegen_threads=${LLVM_ZIG_CODEGEN_THREADS} -Dversion=${VERSION} -Dreported_nodejs_version=${NODEJS_VERSION} @@ -712,6 +762,7 @@ list(APPEND BUN_CPP_SOURCES ${BUN_JAVASCRIPT_OUTPUTS} ${BUN_OBJECT_LUT_OUTPUTS} ${BUN_BINDGEN_CPP_OUTPUTS} + ${BUN_BINDGENV2_CPP_OUTPUTS} ) if(WIN32) @@ -768,7 +819,7 @@ set_target_properties(${bun} PROPERTIES CXX_STANDARD_REQUIRED YES CXX_EXTENSIONS YES CXX_VISIBILITY_PRESET hidden - C_STANDARD 17 + C_STANDARD 23 C_STANDARD_REQUIRED YES VISIBILITY_INLINES_HIDDEN YES ) @@ -849,6 +900,10 @@ if(WIN32) ) endif() +if(USE_MIMALLOC_AS_DEFAULT_ALLOCATOR) + target_compile_definitions(${bun} PRIVATE USE_MIMALLOC=1) +endif() + target_compile_definitions(${bun} PRIVATE _HAS_EXCEPTIONS=0 LIBUS_USE_OPENSSL=1 @@ -990,7 +1045,6 @@ if(APPLE) -Wl,-no_compact_unwind -Wl,-stack_size,0x1200000 -fno-keep-static-consts - -Wl,-map,${bun}.linker-map ) if(DEBUG) @@ -1010,6 +1064,7 @@ if(APPLE) target_link_options(${bun} PUBLIC -dead_strip -dead_strip_dylibs + -Wl,-map,${bun}.linker-map ) endif() endif() @@ -1043,6 +1098,17 @@ if(LINUX) ) endif() + if (ENABLE_LTO) + # We are optimizing for size at a slight debug-ability cost + target_link_options(${bun} PUBLIC + -Wl,--no-eh-frame-hdr + ) + else() + target_link_options(${bun} PUBLIC + -Wl,--eh-frame-hdr + ) + endif() + target_link_options(${bun} PUBLIC --ld-path=${LLD_PROGRAM} -fno-pic @@ -1057,11 +1123,9 @@ if(LINUX) # make debug info faster to load -Wl,--gdb-index -Wl,-z,combreloc - -Wl,--no-eh-frame-hdr -Wl,--sort-section=name -Wl,--hash-style=both -Wl,--build-id=sha1 # Better for debugging than default - -Wl,-Map=${bun}.linker-map ) # don't strip in debug, this seems to be needed so that the Zig std library @@ -1076,6 +1140,7 @@ if(LINUX) if (NOT DEBUG AND NOT ENABLE_ASAN AND NOT ENABLE_VALGRIND) target_link_options(${bun} PUBLIC -Wl,-icf=safe + -Wl,-Map=${bun}.linker-map ) endif() @@ -1397,7 +1462,7 @@ if(NOT BUN_CPP_ONLY) list(APPEND bunFiles ${bun}.dSYM) endif() - if(APPLE OR LINUX) + if((APPLE OR LINUX) AND NOT ENABLE_ASAN) list(APPEND bunFiles ${bun}.linker-map) endif() diff --git a/cmake/targets/BuildHighway.cmake b/cmake/targets/BuildHighway.cmake index 5f8664486d..1796e8f97f 100644 --- a/cmake/targets/BuildHighway.cmake +++ b/cmake/targets/BuildHighway.cmake @@ -4,7 +4,7 @@ register_repository( REPOSITORY google/highway COMMIT - 12b325bc1793dee68ab2157995a690db859fe9e0 + ac0d5d297b13ab1b89f48484fc7911082d76a93f ) set(HIGHWAY_CMAKE_ARGS diff --git a/cmake/targets/BuildLibuv.cmake b/cmake/targets/BuildLibuv.cmake index de95e20955..3072d95532 100644 --- a/cmake/targets/BuildLibuv.cmake +++ b/cmake/targets/BuildLibuv.cmake @@ -4,8 +4,8 @@ register_repository( REPOSITORY libuv/libuv COMMIT - # Corresponds to v1.51.0 - 5152db2cbfeb5582e9c27c5ea1dba2cd9e10759b + # Latest HEAD (includes recursion bug fix #4784) + f3ce527ea940d926c40878ba5de219640c362811 ) if(WIN32) diff --git a/docs/api/http.md b/docs/api/http.md index f3e5b48e55..201e911cfe 100644 --- a/docs/api/http.md +++ b/docs/api/http.md @@ -536,7 +536,7 @@ You can also access the `Server` object from the `fetch` handler. It's the secon const server = Bun.serve({ fetch(req, server) { const ip = server.requestIP(req); - return new Response(`Your IP is ${ip}`); + return new Response(`Your IP is ${ip.address}`); }, }); ``` diff --git a/docs/api/redis.md b/docs/api/redis.md index d8438bc3cf..6e330adcb9 100644 --- a/docs/api/redis.md +++ b/docs/api/redis.md @@ -42,6 +42,7 @@ await client.incr("counter"); By default, the client reads connection information from the following environment variables (in order of precedence): - `REDIS_URL` +- `VALKEY_URL` - If not set, defaults to `"redis://localhost:6379"` ### Connection Lifecycle diff --git a/docs/api/websockets.md b/docs/api/websockets.md index 7b49686e94..d20cbeea84 100644 --- a/docs/api/websockets.md +++ b/docs/api/websockets.md @@ -107,6 +107,8 @@ Bun.serve({ Contextual `data` can be attached to a new WebSocket in the `.upgrade()` call. This data is made available on the `ws.data` property inside the WebSocket handlers. +To strongly type `ws.data`, add a `data` property to the `websocket` handler object. This types `ws.data` across all lifecycle hooks. + ```ts type WebSocketData = { createdAt: number; @@ -114,8 +116,7 @@ type WebSocketData = { authToken: string; }; -// TypeScript: specify the type of `data` -Bun.serve({ +Bun.serve({ fetch(req, server) { const cookies = new Bun.CookieMap(req.headers.get("cookie")!); @@ -131,8 +132,12 @@ Bun.serve({ return undefined; }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as WebSocketData, + // handler called when a message is received async message(ws, message) { + // ws.data is now properly typed as WebSocketData const user = getUserFromToken(ws.data.authToken); await saveMessageToDatabase({ @@ -145,6 +150,10 @@ Bun.serve({ }); ``` +{% callout %} +**Note:** Previously, you could specify the type of `ws.data` using a type parameter on `Bun.serve`, like `Bun.serve({...})`. This pattern was removed due to [a limitation in TypeScript](https://github.com/microsoft/TypeScript/issues/26242) in favor of the `data` property shown above. +{% /callout %} + To connect to this server from the browser, create a new `WebSocket`. ```ts#browser.js @@ -164,7 +173,7 @@ socket.addEventListener("message", event => { Bun's `ServerWebSocket` implementation implements a native publish-subscribe API for topic-based broadcasting. Individual sockets can `.subscribe()` to a topic (specified with a string identifier) and `.publish()` messages to all other subscribers to that topic (excluding itself). This topic-based broadcast API is similar to [MQTT](https://en.wikipedia.org/wiki/MQTT) and [Redis Pub/Sub](https://redis.io/topics/pubsub). ```ts -const server = Bun.serve<{ username: string }>({ +const server = Bun.serve({ fetch(req, server) { const url = new URL(req.url); if (url.pathname === "/chat") { @@ -179,6 +188,9 @@ const server = Bun.serve<{ username: string }>({ return new Response("Hello world"); }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { username: string }, + open(ws) { const msg = `${ws.data.username} has entered the chat`; ws.subscribe("the-group-chat"); diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 56f1ac8ed7..097c64f978 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -586,12 +586,41 @@ Codesign support requires Bun v1.2.4 or newer. {% /callout %} +## Code splitting + +Standalone executables support code splitting. Use `--compile` with `--splitting` to create an executable that loads code-split chunks at runtime. + +```bash +$ bun build --compile --splitting ./src/entry.ts --outdir ./build +``` + +{% codetabs %} + +```ts#src/entry.ts +console.log("Entrypoint loaded"); +const lazy = await import("./lazy.ts"); +lazy.hello(); +``` + +```ts#src/lazy.ts +export function hello() { + console.log("Lazy module loaded"); +} +``` + +{% /codetabs %} + +```bash +$ ./build/entry +Entrypoint loaded +Lazy module loaded +``` + ## Unsupported CLI arguments Currently, the `--compile` flag can only accept a single entrypoint at a time and does not support the following flags: -- `--outdir` — use `outfile` instead. -- `--splitting` +- `--outdir` — use `outfile` instead (except when using with `--splitting`). - `--public-path` - `--target=node` or `--target=browser` - `--no-bundle` - we always bundle everything into the executable. diff --git a/docs/bundler/index.md b/docs/bundler/index.md index 53a5eaab2e..9b581ccfe0 100644 --- a/docs/bundler/index.md +++ b/docs/bundler/index.md @@ -1600,7 +1600,7 @@ interface BuildConfig { publicPath?: string; define?: Record; loader?: { [k in string]: Loader }; - sourcemap?: "none" | "linked" | "inline" | "external" | "linked" | boolean; // default: "none", true -> "inline" + sourcemap?: "none" | "linked" | "inline" | "external" | boolean; // default: "none", true -> "inline" /** * package.json `exports` conditions used when resolving imports * diff --git a/docs/cli/init.md b/docs/cli/init.md index 0253333b73..8b419ae37d 100644 --- a/docs/cli/init.md +++ b/docs/cli/init.md @@ -2,20 +2,29 @@ Scaffold an empty Bun project with the interactive `bun init` command. ```bash $ bun init -bun init helps you get started with a minimal project and tries to -guess sensible defaults. Press ^C anytime to quit. -package name (quickstart): -entry point (index.ts): +? Select a project template - Press return to submit. +❯ Blank + React + Library -Done! A package.json file was saved in the current directory. - + index.ts - + .gitignore - + tsconfig.json (for editor auto-complete) - + README.md +✓ Select a project template: Blank + ++ .gitignore ++ index.ts ++ tsconfig.json (for editor autocomplete) ++ README.md To get started, run: - bun run index.ts + + bun run index.ts + +bun install v$BUN_LATEST_VERSION + ++ @types/bun@$BUN_LATEST_VERSION ++ typescript@5.9.2 + +7 packages installed ``` Press `enter` to accept the default answer for each prompt, or pass the `-y` flag to auto-accept the defaults. diff --git a/docs/cli/install.md b/docs/cli/install.md index 0ad692ac62..68578a1c22 100644 --- a/docs/cli/install.md +++ b/docs/cli/install.md @@ -221,6 +221,38 @@ Bun uses a global cache at `~/.bun/install/cache/` to minimize disk usage. Packa For complete documentation refer to [Package manager > Global cache](https://bun.com/docs/install/cache). +## Minimum release age + +To protect against supply chain attacks where malicious packages are quickly published, you can configure a minimum age requirement for npm packages. Package versions published more recently than the specified threshold (in seconds) will be filtered out during installation. + +```bash +# Only install package versions published at least 3 days ago +$ bun add @types/bun --minimum-release-age 259200 # seconds +``` + +You can also configure this in `bunfig.toml`: + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 # seconds + +# Exclude trusted packages from the age gate +minimumReleaseAgeExcludes = ["@types/node", "typescript"] +``` + +When the minimum age filter is active: + +- Only affects new package resolution - existing packages in `bun.lock` remain unchanged +- All dependencies (direct and transitive) are filtered to meet the age requirement when being resolved +- When versions are blocked by the age gate, a stability check detects rapid bugfix patterns + - If multiple versions were published close together just outside your age gate, it extends the filter to skip those potentially unstable versions and selects an older, more mature version + - Searches up to 7 days after the age gate, however if still finding rapid releases it ignores stability check + - Exact version requests (like `package@1.1.1`) still respect the age gate but bypass the stability check +- Versions without a `time` field are treated as passing the age check (npm registry should always provide timestamps) + +For more advanced security scanning, including integration with services & custom filtering, see [Package manager > Security Scanner API](https://bun.com/docs/install/security-scanner-api). + ## Configuration The default behavior of `bun install` can be configured in `bunfig.toml`. The default values are shown below. @@ -255,6 +287,10 @@ concurrentScripts = 16 # (cpu count or GOMAXPROCS) x2 # installation strategy: "hoisted" or "isolated" # default: "hoisted" linker = "hoisted" + +# minimum age config +minimumReleaseAge = 259200 # seconds +minimumReleaseAgeExcludes = ["@types/node", "typescript"] ``` ## CI/CD diff --git a/docs/cli/publish.md b/docs/cli/publish.md index c2d9f75cf1..8b930f290f 100644 --- a/docs/cli/publish.md +++ b/docs/cli/publish.md @@ -84,14 +84,12 @@ $ bun publish --dry-run ### `--tolerate-republish` -The `--tolerate-republish` flag makes `bun publish` exit with code 0 instead of code 1 when attempting to republish over an existing version number. This is useful in automated workflows where republishing the same version might occur and should not be treated as an error. +Exit with code 0 instead of 1 if the package version already exists. Useful in CI/CD where jobs may be re-run. ```sh $ bun publish --tolerate-republish ``` -Without this flag, attempting to publish a version that already exists will result in an error and exit code 1. With this flag, the command will exit successfully even when trying to republish an existing version. - ### `--gzip-level` Specify the level of gzip compression to use when packing the package. Only applies to `bun publish` without a tarball path argument. Values range from `0` to `9` (default is `9`). diff --git a/docs/guides/websocket/context.md b/docs/guides/websocket/context.md index ef6fe9b009..2dc35e6925 100644 --- a/docs/guides/websocket/context.md +++ b/docs/guides/websocket/context.md @@ -7,7 +7,7 @@ When building a WebSocket server, it's typically necessary to store some identif With [Bun.serve()](https://bun.com/docs/api/websockets#contextual-data), this "contextual data" is set when the connection is initially upgraded by passing a `data` parameter in the `server.upgrade()` call. ```ts -Bun.serve<{ socketId: number }>({ +Bun.serve({ fetch(req, server) { const success = server.upgrade(req, { data: { @@ -20,6 +20,9 @@ Bun.serve<{ socketId: number }>({ // ... }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { socketId: number }, + // define websocket handlers async message(ws, message) { // the contextual data is available as the `data` property @@ -41,8 +44,7 @@ type WebSocketData = { userId: string; }; -// TypeScript: specify the type of `data` -Bun.serve({ +Bun.serve({ async fetch(req, server) { // use a library to parse cookies const cookies = parseCookies(req.headers.get("Cookie")); @@ -60,6 +62,9 @@ Bun.serve({ if (upgraded) return undefined; }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as WebSocketData, + async message(ws, message) { // save the message to a database await saveMessageToDatabase({ diff --git a/docs/guides/websocket/pubsub.md b/docs/guides/websocket/pubsub.md index b291a2214e..b0adb26d65 100644 --- a/docs/guides/websocket/pubsub.md +++ b/docs/guides/websocket/pubsub.md @@ -7,7 +7,7 @@ Bun's server-side `WebSocket` API provides a native pub-sub API. Sockets can be This code snippet implements a simple single-channel chat server. ```ts -const server = Bun.serve<{ username: string }>({ +const server = Bun.serve({ fetch(req, server) { const cookies = req.headers.get("cookie"); const username = getUsernameFromCookies(cookies); @@ -17,6 +17,9 @@ const server = Bun.serve<{ username: string }>({ return new Response("Hello world"); }, websocket: { + // TypeScript: specify the type of ws.data like this + data: {} as { username: string }, + open(ws) { const msg = `${ws.data.username} has entered the chat`; ws.subscribe("the-group-chat"); diff --git a/docs/guides/websocket/simple.md b/docs/guides/websocket/simple.md index b14434deb0..81b2876ddc 100644 --- a/docs/guides/websocket/simple.md +++ b/docs/guides/websocket/simple.md @@ -7,7 +7,7 @@ Start a simple WebSocket server using [`Bun.serve`](https://bun.com/docs/api/htt Inside `fetch`, we attempt to upgrade incoming `ws:` or `wss:` requests to WebSocket connections. ```ts -const server = Bun.serve<{ authToken: string }>({ +const server = Bun.serve({ fetch(req, server) { const success = server.upgrade(req); if (success) { diff --git a/docs/install/index.md b/docs/install/index.md index 83a387e9b1..3ecd5c7838 100644 --- a/docs/install/index.md +++ b/docs/install/index.md @@ -89,6 +89,12 @@ $ bun install --linker isolated Isolated installs create strict dependency isolation similar to pnpm, preventing phantom dependencies and ensuring more deterministic builds. For complete documentation, see [Isolated installs](https://bun.com/docs/install/isolated). +To protect against supply chain attacks, set a minimum age (in seconds) for package versions: + +```bash +$ bun install --minimum-release-age 259200 # 3 days +``` + {% details summary="Configuring behavior" %} The default behavior of `bun install` can be configured in `bunfig.toml`: @@ -122,6 +128,12 @@ concurrentScripts = 16 # (cpu count or GOMAXPROCS) x2 # installation strategy: "hoisted" or "isolated" # default: "hoisted" linker = "hoisted" + +# minimum package age in seconds (protects against supply chain attacks) +minimumReleaseAge = 259200 # 3 days + +# exclude packages from age requirement +minimumReleaseAgeExcludes = ["@types/node", "typescript"] ``` {% /details %} diff --git a/docs/install/isolated.md b/docs/install/isolated.md index 0cc06c9722..d58be16c35 100644 --- a/docs/install/isolated.md +++ b/docs/install/isolated.md @@ -36,7 +36,10 @@ linker = "isolated" ### Default behavior -By default, Bun uses the **hoisted** installation strategy for all projects. To use isolated installs, you must explicitly specify the `--linker isolated` flag or set it in your configuration file. +- **Workspaces**: Bun uses **isolated** installs by default to prevent hoisting-related bugs +- **Single projects**: Bun uses **hoisted** installs by default + +To override the default, use `--linker hoisted` or `--linker isolated`, or set it in your configuration file. ## How isolated installs work @@ -174,14 +177,13 @@ The main difference is that Bun uses symlinks in `node_modules` while pnpm uses ## When to use isolated installs -**Use isolated installs when:** +**Isolated installs are the default for workspaces.** You may want to explicitly enable them for single projects when: -- Working in monorepos with multiple packages - Strict dependency management is required - Preventing phantom dependencies is important - Building libraries that need deterministic dependencies -**Use hoisted installs when:** +**Switch to hoisted installs (including for workspaces) when:** - Working with legacy code that assumes flat `node_modules` - Compatibility with existing build tools is required diff --git a/docs/install/workspaces.md b/docs/install/workspaces.md index b9e296d051..2e3c9c8238 100644 --- a/docs/install/workspaces.md +++ b/docs/install/workspaces.md @@ -38,9 +38,21 @@ In the root `package.json`, the `"workspaces"` key is used to indicate which sub ``` {% callout %} -**Glob support** — Bun supports full glob syntax in `"workspaces"` (see [here](https://bun.com/docs/api/glob#supported-glob-patterns) for a comprehensive list of supported syntax), _except_ for exclusions (e.g. `!**/excluded/**`), which are not implemented yet. +**Glob support** — Bun supports full glob syntax in `"workspaces"`, including negative patterns (e.g. `!**/excluded/**`). See [here](https://bun.com/docs/api/glob#supported-glob-patterns) for a comprehensive list of supported syntax. {% /callout %} +```json +{ + "name": "my-project", + "version": "1.0.0", + "workspaces": [ + "packages/**", + "!packages/**/test/**", + "!packages/**/template/**" + ] +} +``` + Each workspace has it's own `package.json`. When referencing other packages in the monorepo, semver or workspace protocols (e.g. `workspace:*`) can be used as the version field in your `package.json`. ```json diff --git a/docs/quickstart.md b/docs/quickstart.md index 2cf48aa4eb..8ded324e82 100644 --- a/docs/quickstart.md +++ b/docs/quickstart.md @@ -9,20 +9,29 @@ Run `bun init` to scaffold a new project. It's an interactive tool; for this tut ```bash $ bun init -bun init helps you get started with a minimal project and tries to -guess sensible defaults. Press ^C anytime to quit. -package name (quickstart): -entry point (index.ts): +? Select a project template - Press return to submit. +❯ Blank + React + Library -Done! A package.json file was saved in the current directory. - + index.ts - + .gitignore - + tsconfig.json (for editor auto-complete) - + README.md +✓ Select a project template: Blank + ++ .gitignore ++ index.ts ++ tsconfig.json (for editor autocomplete) ++ README.md To get started, run: - bun run index.ts + + bun run index.ts + +bun install v$BUN_LATEST_VERSION + ++ @types/bun@$BUN_LATEST_VERSION ++ typescript@5.9.2 + +7 packages installed ``` Since our entry point is a `*.ts` file, Bun generates a `tsconfig.json` for you. If you're using plain JavaScript, it will generate a [`jsconfig.json`](https://code.visualstudio.com/docs/languages/jsconfig) instead. @@ -88,11 +97,15 @@ Bun can also execute `"scripts"` from your `package.json`. Add the following scr "name": "quickstart", "module": "index.ts", "type": "module", + "private": true, + "scripts": { + "start": "bun run index.ts" + }, "devDependencies": { "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" } } ``` diff --git a/docs/runtime/bunfig.md b/docs/runtime/bunfig.md index 11c47d814c..5a911668ab 100644 --- a/docs/runtime/bunfig.md +++ b/docs/runtime/bunfig.md @@ -249,6 +249,81 @@ This is useful for: The `--concurrent` CLI flag will override this setting when specified. +### `test.onlyFailures` + +When enabled, only failed tests are displayed in the output. This helps reduce noise in large test suites by hiding passing tests. Default `false`. + +```toml +[test] +onlyFailures = true +``` + +This is equivalent to using the `--only-failures` flag when running `bun test`. + +### `test.reporter` + +Configure the test reporter settings. + +#### `test.reporter.dots` + +Enable the dots reporter, which displays a compact output showing a dot for each test. Default `false`. + +```toml +[test.reporter] +dots = true +``` + +#### `test.reporter.junit` + +Enable JUnit XML reporting and specify the output file path. + +```toml +[test.reporter] +junit = "test-results.xml" +``` + +This generates a JUnit XML report that can be consumed by CI systems and other tools. + +### `test.randomize` + +Run tests in random order. Default `false`. + +```toml +[test] +randomize = true +``` + +This helps catch bugs related to test interdependencies by running tests in a different order each time. When combined with `seed`, the random order becomes reproducible. + +The `--randomize` CLI flag will override this setting when specified. + +### `test.seed` + +Set the random seed for test randomization. This option requires `randomize` to be `true`. + +```toml +[test] +randomize = true +seed = 2444615283 +``` + +Using a seed makes the randomized test order reproducible across runs, which is useful for debugging flaky tests. When you encounter a test failure with randomization enabled, you can use the same seed to reproduce the exact test order. + +The `--seed` CLI flag will override this setting when specified. + +### `test.rerunEach` + +Re-run each test file a specified number of times. Default `0` (run once). + +```toml +[test] +rerunEach = 3 +``` + +This is useful for catching flaky tests or non-deterministic behavior. Each test file will be executed the specified number of times. + +The `--rerun-each` CLI flag will override this setting when specified. + ## Package manager Package management is a complex issue; to support a range of use cases, the behavior of `bun install` can be configured under the `[install]` section. @@ -570,6 +645,20 @@ Valid values are: {% /table %} +### `install.minimumReleaseAge` + +Configure a minimum age (in seconds) for npm package versions. Package versions published more recently than this threshold will be filtered out during installation. Default is `null` (disabled). + +```toml +[install] +# Only install package versions published at least 3 days ago +minimumReleaseAge = 259200 +# These packages will bypass the 3-day minimum age requirement +minimumReleaseAgeExcludes = ["@types/bun", "typescript"] +``` + +For more details see [Minimum release age](https://bun.com/docs/cli/install#minimum-release-age) in the install documentation. +