diff --git a/docs/api/binary-data.md b/docs/api/binary-data.md index bd9bed578b..1480585afe 100644 --- a/docs/api/binary-data.md +++ b/docs/api/binary-data.md @@ -34,6 +34,11 @@ Below is a quick "cheat sheet" that doubles as a table of contents. Click an ite - [`BunFile`](#bunfile) - _Bun only_. A subclass of `Blob` that represents a lazily-loaded file on disk. Created with `Bun.file(path)`. +--- + +- [`CryptoHasher`](#buncryptohasher) +- _Bun only_. Hardware-accelerated cryptographic hash functions with streaming support. More direct interface than `crypto.createHash()`. + {% /table %} ## `ArrayBuffer` and views @@ -1020,6 +1025,160 @@ To split a `ReadableStream` into two streams that can be consumed independently: const [a, b] = stream.tee(); ``` +## `Bun.CryptoHasher` + +_Bun only_. Hardware-accelerated cryptographic hash functions. This is the class used internally by `crypto.createHash()` and provides a more direct interface for high-performance hashing operations. + +### Creating a hasher + +```ts +import { CryptoHasher } from "bun"; + +const hasher = new CryptoHasher("sha256"); +console.log(hasher.algorithm); // => "sha256" +console.log(hasher.byteLength); // => 32 (SHA-256 output size) +``` + +### Updating with data + +The `update()` method adds data to be hashed. It can be called multiple times to process data incrementally. + +```ts +const hasher = new CryptoHasher("sha256"); + +// Add data in chunks +hasher.update("Hello"); +hasher.update(" "); +hasher.update("World"); + +// Different data types supported +hasher.update(new Uint8Array([1, 2, 3])); +hasher.update(Buffer.from("more data")); +``` + +### Getting the final hash + +Use `digest()` to finalize the hash and get the result. This resets the hasher so it can be reused. + +```ts +const hasher = new CryptoHasher("sha256"); +hasher.update("Hello World"); + +// Get hash as different formats +const hexHash = hasher.digest("hex"); +console.log(hexHash); // => "a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b57b277d9ad9f146e" + +// Hasher is reset, can be reused +hasher.update("New data"); +const buffer = hasher.digest(); // Returns Buffer +``` + +### Copying hashers + +The `copy()` method creates a deep copy of the hasher's current state, useful for computing multiple hashes from a common prefix. + +```ts +const hasher = new CryptoHasher("sha256"); +hasher.update("Common prefix: "); + +// Create copies for different suffixes +const copy1 = hasher.copy(); +const copy2 = hasher.copy(); + +copy1.update("suffix 1"); +copy2.update("suffix 2"); + +console.log(copy1.digest("hex")); // Hash of "Common prefix: suffix 1" +console.log(copy2.digest("hex")); // Hash of "Common prefix: suffix 2" +``` + +### Supported algorithms + +The following cryptographic hash algorithms are supported: + +```ts +// SHA family +new CryptoHasher("sha1"); +new CryptoHasher("sha224"); +new CryptoHasher("sha256"); +new CryptoHasher("sha384"); +new CryptoHasher("sha512"); +new CryptoHasher("sha512-256"); + +// BLAKE2 family (very fast) +new CryptoHasher("blake2b256"); +new CryptoHasher("blake2b512"); + +// MD family (not recommended for security) +new CryptoHasher("md4"); +new CryptoHasher("md5"); +``` + +### Performance characteristics + +`CryptoHasher` uses hardware acceleration when available and is optimized for high throughput: + +```ts +// Benchmark different algorithms +const algorithms = ["sha256", "blake2b256", "md5"]; +const data = "x".repeat(1024 * 1024); // 1MB of data + +for (const algo of algorithms) { + const hasher = new CryptoHasher(algo); + console.time(algo); + hasher.update(data); + const hash = hasher.digest("hex"); + console.timeEnd(algo); + console.log(`${algo}: ${hash.slice(0, 16)}...`); +} +``` + +### Streaming large files + +For processing large files efficiently: + +```ts +async function hashFile(path: string, algorithm = "sha256"): Promise { + const hasher = new CryptoHasher(algorithm); + const file = Bun.file(path); + + // Process file in chunks + const stream = file.stream(); + const reader = stream.getReader(); + + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + hasher.update(value); + } + } finally { + reader.releaseLock(); + } + + return hasher.digest("hex"); +} + +const hash = await hashFile("large-file.bin"); +console.log(`File hash: ${hash}`); +``` + +### Integration with crypto module + +`CryptoHasher` is the underlying implementation for Node.js-compatible crypto functions: + +```ts +import crypto from "crypto"; +import { CryptoHasher } from "bun"; + +// These are equivalent: +const nodeHash = crypto.createHash("sha256").update("data").digest("hex"); +const bunHash = new CryptoHasher("sha256").update("data").digest("hex"); + +console.log(nodeHash === bunHash); // => true +``` + +
+ + + +
+``` + +### Error handling + +```ts +import { CSRF } from "bun"; + +try { + // Generate token + const token = CSRF.generate("my-secret"); + + // Verify token + const isValid = CSRF.verify(token, { secret: "my-secret" }); +} catch (error) { + if (error.message.includes("secret")) { + console.error("Invalid secret provided"); + } else { + console.error("CSRF operation failed:", error.message); + } +} +``` + +Common error scenarios: +- Empty or invalid token strings throw verification errors +- Empty secret strings throw generation/verification errors +- Invalid encoding options are handled gracefully +- Malformed tokens return `false` rather than throwing diff --git a/docs/bundler/executables.md b/docs/bundler/executables.md index 1fc4e1d130..3397630d9a 100644 --- a/docs/bundler/executables.md +++ b/docs/bundler/executables.md @@ -384,14 +384,111 @@ import "./icon.png" with { type: "file" }; import { embeddedFiles } from "bun"; console.log(embeddedFiles[0].name); // `icon-${hash}.png` +console.log(embeddedFiles[0].size); // File size in bytes +console.log(embeddedFiles[0].type); // MIME type (e.g., "image/png") ``` -`Bun.embeddedFiles` returns an array of `Blob` objects which you can use to get the size, contents, and other properties of the files. +`Bun.embeddedFiles` returns a read-only array of `Blob` objects sorted lexicographically by filename. Each `Blob` provides access to the embedded file's contents and metadata. ```ts -embeddedFiles: Blob[] +const embeddedFiles: ReadonlyArray ``` +**Properties of embedded file `Blob`s:** + +- `name` (`string`): The filename with hash suffix (e.g., `icon-a1b2c3.png`) +- `size` (`number`): File size in bytes +- `type` (`string`): MIME type automatically detected from file extension +- Standard `Blob` methods: `text()`, `arrayBuffer()`, `bytes()`, `stream()`, `slice()` + +### Working with embedded files + +```js +import "./assets/logo.svg" with { type: "file" }; +import "./assets/data.json" with { type: "file" }; +import { embeddedFiles } from "bun"; + +// Find a specific embedded file +const logo = embeddedFiles.find(file => file.name.startsWith("logo")); +if (logo) { + console.log(`Logo size: ${logo.size} bytes`); + const logoContent = await logo.text(); +} + +// Process all embedded files +for (const file of embeddedFiles) { + console.log(`File: ${file.name}`); + console.log(` Size: ${file.size} bytes`); + console.log(` Type: ${file.type}`); + + // Read file content based on type + if (file.type.startsWith("text/") || file.type === "application/json") { + const content = await file.text(); + console.log(` Content preview: ${content.slice(0, 100)}...`); + } +} + +// Convert to different formats +const dataFile = embeddedFiles.find(f => f.name.startsWith("data")); +if (dataFile) { + const buffer = await dataFile.arrayBuffer(); + const bytes = await dataFile.bytes(); + const stream = dataFile.stream(); +} +``` + +### Serving embedded files over HTTP + +Embedded files can be served directly in HTTP responses: + +```js +import "./public/favicon.ico" with { type: "file" }; +import "./public/robots.txt" with { type: "file" }; +import { embeddedFiles } from "bun"; + +const server = Bun.serve({ + port: 3000, + fetch(req) { + const url = new URL(req.url); + const filename = url.pathname.slice(1); // Remove leading slash + + // Find embedded file by filename (ignoring hash) + const file = embeddedFiles.find(f => + f.name.includes(filename.split('.')[0]) + ); + + if (file) { + return new Response(file, { + headers: { + "Content-Type": file.type, + "Content-Length": file.size.toString(), + "Cache-Control": "public, max-age=31536000" // 1 year cache + } + }); + } + + return new Response("Not found", { status: 404 }); + } +}); +``` + +### Important notes + +- **Read-only**: The `embeddedFiles` array and individual files cannot be modified at runtime +- **Empty when not compiled**: Returns an empty array when running with `bun run` (not compiled) +- **Hash suffixes**: Filenames include content hashes for cache busting (e.g., `style-a1b2c3.css`) +- **MIME type detection**: File types are automatically detected from file extensions +- **Memory efficient**: Files are lazily loaded - accessing content triggers reading from the embedded data +- **Lexicographic ordering**: Files are sorted alphabetically by their embedded names + +### Use cases + +- **Static assets**: Serve CSS, images, fonts directly from the executable +- **Configuration files**: Embed JSON/YAML config that's read at runtime +- **Templates**: Include HTML/text templates in the binary +- **Data files**: Ship with necessary data files without external dependencies +- **Web assets**: Bundle frontend resources with backend services + The list of embedded files excludes bundled source code like `.ts` and `.js` files. #### Content hash diff --git a/docs/bundler/macros.md b/docs/bundler/macros.md index eb4ee464cf..097c99c27a 100644 --- a/docs/bundler/macros.md +++ b/docs/bundler/macros.md @@ -327,3 +327,31 @@ export { Head }; ``` {% /codetabs %} + +## Advanced Macro APIs + +### `Bun.registerMacro(id, macro)` + +Registers a macro function with a specific numeric ID for internal use by the bundler. This is a low-level API primarily used internally during the bundling process. + +```js +// This API is primarily used internally by the bundler +// Most users should use import statements with { type: "macro" } instead +``` + +**Parameters:** +- `id` (`number`): A unique numeric identifier for the macro (must be positive, non-zero) +- `macro` (`Function`): The macro function to register + +**Returns:** `undefined` + +{% callout type="warning" %} +**Note** — `Bun.registerMacro()` is an internal API used by Bun's bundler during code generation. User code should not call this function directly. Instead, use the standard `import { myMacro } from "./macro.ts" with { type: "macro" }` syntax to define and use macros. +{% /callout %} + +**Security considerations:** +- Only callable functions are accepted as macro arguments +- Invalid IDs (0, -1, or non-numeric values) will throw an error +- This function requires exactly 2 arguments + +The bundler automatically calls `registerMacro()` when it encounters macro imports during the bundling process, assigning unique IDs to each macro and registering them for execution. diff --git a/docs/guides/util/zstd.md b/docs/guides/util/zstd.md new file mode 100644 index 0000000000..7a32ab923e --- /dev/null +++ b/docs/guides/util/zstd.md @@ -0,0 +1,214 @@ +--- +name: Compress and decompress data with Zstandard (zstd) +--- + +Bun provides fast, built-in support for [Zstandard compression](https://facebook.github.io/zstd/), a high-performance compression algorithm developed by Facebook. Zstandard offers an excellent balance of compression ratio, speed, and memory usage. + +## Synchronous compression + +Use `Bun.zstdCompressSync()` to synchronously compress data with Zstandard. + +```ts +const data = "Hello, world! ".repeat(100); +const compressed = Bun.zstdCompressSync(data); +// => Uint8Array + +console.log(`Original: ${data.length} bytes`); +console.log(`Compressed: ${compressed.length} bytes`); +console.log(`Compression ratio: ${(data.length / compressed.length).toFixed(2)}x`); +``` + +The function accepts strings, `Uint8Array`, `ArrayBuffer`, `Buffer`, and other binary data types: + +```ts +// String +const textCompressed = Bun.zstdCompressSync("Hello, world!"); + +// Buffer +const bufferCompressed = Bun.zstdCompressSync(Buffer.from("Hello, world!")); + +// Uint8Array +const uint8Compressed = Bun.zstdCompressSync(new TextEncoder().encode("Hello, world!")); +``` + +## Synchronous decompression + +Use `Bun.zstdDecompressSync()` to decompress Zstandard-compressed data: + +```ts +const compressed = Bun.zstdCompressSync("Hello, world!"); +const decompressed = Bun.zstdDecompressSync(compressed); + +// Convert back to string +const text = new TextDecoder().decode(decompressed); +console.log(text); // => "Hello, world!" +``` + +## Asynchronous compression + +Use `Bun.zstdCompress()` for asynchronous compression. This is useful for large data that might block the event loop: + +```ts +const data = "Hello, world! ".repeat(10000); +const compressed = await Bun.zstdCompress(data); +// => Promise + +console.log(`Compressed ${data.length} bytes to ${compressed.length} bytes`); +``` + +## Asynchronous decompression + +Use `Bun.zstdDecompress()` for asynchronous decompression: + +```ts +const compressed = await Bun.zstdCompress("Hello, world!"); +const decompressed = await Bun.zstdDecompress(compressed); + +const text = new TextDecoder().decode(decompressed); +console.log(text); // => "Hello, world!" +``` + +## Compression levels + +Zstandard supports compression levels from 1 to 22, where: +- **Level 1**: Fastest compression, larger file size +- **Level 3**: Default level (good balance of speed and compression) +- **Level 19**: Very high compression, slower +- **Level 22**: Maximum compression, slowest + +```ts +const data = "Hello, world! ".repeat(1000); + +// Fast compression (level 1) +const fast = Bun.zstdCompressSync(data, { level: 1 }); + +// Balanced compression (level 3, default) +const balanced = Bun.zstdCompressSync(data, { level: 3 }); + +// High compression (level 19) +const small = Bun.zstdCompressSync(data, { level: 19 }); + +console.log(`Fast (level 1): ${fast.length} bytes`); +console.log(`Balanced (level 3): ${balanced.length} bytes`); +console.log(`High (level 19): ${small.length} bytes`); +``` + +The same level options work for async compression: + +```ts +const compressed = await Bun.zstdCompress(data, { level: 19 }); +``` + +## Error handling + +Both sync and async functions will throw errors for invalid input: + +```ts +try { + // Invalid compression level + Bun.zstdCompressSync("data", { level: 0 }); // Error: level must be 1-22 +} catch (error) { + console.error(error.message); // => "Compression level must be between 1 and 22" +} + +try { + // Invalid compressed data + Bun.zstdDecompressSync("not compressed data"); +} catch (error) { + console.error("Decompression failed:", error.message); +} +``` + +For async functions, handle errors with try/catch or `.catch()`: + +```ts +try { + await Bun.zstdDecompress("invalid data"); +} catch (error) { + console.error("Async decompression failed:", error.message); +} +``` + +## Working with files + +Compress and decompress files efficiently: + +```ts +// Compress a file +const file = Bun.file("large-file.txt"); +const data = await file.bytes(); +const compressed = await Bun.zstdCompress(data, { level: 6 }); +await Bun.write("large-file.txt.zst", compressed); + +// Decompress a file +const compressedFile = Bun.file("large-file.txt.zst"); +const compressedData = await compressedFile.bytes(); +const decompressed = await Bun.zstdDecompress(compressedData); +await Bun.write("large-file-restored.txt", decompressed); +``` + +## Performance characteristics + +Zstandard offers excellent performance compared to other compression algorithms: + +- **Speed**: Faster decompression than gzip, competitive compression speed +- **Compression ratio**: Better than gzip, similar to or better than brotli +- **Memory usage**: Moderate memory requirements +- **Real-time friendly**: Suitable for real-time applications due to fast decompression + +Example performance comparison for a 1MB text file: + +```ts +const data = "Sample data ".repeat(100000); // ~1MB + +console.time("zstd compress"); +const zstdCompressed = Bun.zstdCompressSync(data, { level: 3 }); +console.timeEnd("zstd compress"); + +console.time("gzip compress"); +const gzipCompressed = Bun.gzipSync(data); +console.timeEnd("gzip compress"); + +console.log(`Zstandard: ${zstdCompressed.length} bytes`); +console.log(`Gzip: ${gzipCompressed.length} bytes`); +``` + +## HTTP compression + +Zstandard is supported in modern browsers and can be used for HTTP compression. When building web servers, check the `Accept-Encoding` header: + +```ts +const server = Bun.serve({ + async fetch(req) { + const acceptEncoding = req.headers.get("Accept-Encoding") || ""; + const content = "Large response content..."; + + if (acceptEncoding.includes("zstd")) { + const compressed = await Bun.zstdCompress(content, { level: 6 }); + return new Response(compressed, { + headers: { + "Content-Encoding": "zstd", + "Content-Type": "text/plain" + } + }); + } + + return new Response(content); + } +}); +``` + +## When to use Zstandard + +Choose Zstandard when you need: + +- **Better compression ratios** than gzip with similar or better speed +- **Fast decompression** for frequently accessed compressed data +- **Streaming support** (via Node.js compatible `zlib` streams) +- **Modern web applications** where browser support allows + +For maximum compatibility, consider falling back to gzip for older clients. + +--- + +See [Docs > API > Utils](/docs/api/utils) for more compression utilities including gzip, deflate, and brotli. \ No newline at end of file diff --git a/docs/runtime/bun-apis.md b/docs/runtime/bun-apis.md index ce768bb092..4be2c0a759 100644 --- a/docs/runtime/bun-apis.md +++ b/docs/runtime/bun-apis.md @@ -175,7 +175,7 @@ Click the link in the right column to jump to the associated documentation. --- - Compression -- [`Bun.gzipSync()`](https://bun.com/docs/api/utils#bun-gzipsync), [`Bun.gunzipSync()`](https://bun.com/docs/api/utils#bun-gunzipsync), [`Bun.deflateSync()`](https://bun.com/docs/api/utils#bun-deflatesync), [`Bun.inflateSync()`](https://bun.com/docs/api/utils#bun-inflatesync), `Bun.zstdCompressSync()`, `Bun.zstdDecompressSync()`, `Bun.zstdCompress()`, `Bun.zstdDecompress()` +- [`Bun.gzipSync()`](https://bun.com/docs/api/utils#bun-gzipsync), [`Bun.gunzipSync()`](https://bun.com/docs/api/utils#bun-gunzipsync), [`Bun.deflateSync()`](https://bun.com/docs/api/utils#bun-deflatesync), [`Bun.inflateSync()`](https://bun.com/docs/api/utils#bun-inflatesync), [`Bun.zstdCompressSync()`](https://bun.com/docs/api/utils#bun-zstdcompresssync), [`Bun.zstdDecompressSync()`](https://bun.com/docs/api/utils#bun-zstddecompresssync), [`Bun.zstdCompress()`](https://bun.com/docs/api/utils#bun-zstdcompress), [`Bun.zstdDecompress()`](https://bun.com/docs/api/utils#bun-zstddecompress) --- diff --git a/docs/runtime/shell.md b/docs/runtime/shell.md index ebddd8e2c9..fc1448be13 100644 --- a/docs/runtime/shell.md +++ b/docs/runtime/shell.md @@ -600,6 +600,60 @@ user-provided input before passing it as an argument to an external command. The responsibility for validating arguments rests with your application code. {% /callout %} +## Advanced APIs + +### `Bun.createParsedShellScript(script, args)` + +Creates a pre-parsed shell script object that can be used with `Bun.createShellInterpreter()` for more advanced shell execution control. + +```js +import { createParsedShellScript } from "bun"; + +// Parse a shell script +const parsed = createParsedShellScript("echo hello", ["world"]); +``` + +**Parameters:** +- `script` (`string`): The shell script string to parse +- `args` (`string[]`): Array of arguments to interpolate into the script + +**Returns:** `ParsedShellScript` - A parsed shell script object + +This API is primarily used internally by the `$` template literal, but can be useful for cases where you want to pre-parse shell commands or build custom shell execution workflows. + +### `Bun.createShellInterpreter(options)` + +Creates a shell interpreter instance for executing parsed shell scripts with custom resolve/reject handlers. + +```js +import { createParsedShellScript, createShellInterpreter } from "bun"; + +const parsed = createParsedShellScript("echo hello", ["world"]); + +const interpreter = createShellInterpreter( + (exitCode, stdout, stderr) => { + // Handle successful completion + console.log(`Exit code: ${exitCode}`); + console.log(`Stdout: ${stdout.toString()}`); + }, + (exitCode, stdout, stderr) => { + // Handle errors + console.error(`Command failed with code: ${exitCode}`); + console.error(`Stderr: ${stderr.toString()}`); + }, + parsed +); +``` + +**Parameters:** +- `resolve` (`(exitCode: number, stdout: Buffer, stderr: Buffer) => void`): Callback for successful execution +- `reject` (`(exitCode: number, stdout: Buffer, stderr: Buffer) => void`): Callback for failed execution +- `parsedScript` (`ParsedShellScript`): The parsed shell script to execute + +**Returns:** `ShellInterpreter` - A shell interpreter instance + +This low-level API gives you direct control over shell execution and is primarily used internally by Bun Shell. Most users should use the `$` template literal instead, which provides a higher-level interface. + ## Credits Large parts of this API were inspired by [zx](https://github.com/google/zx), [dax](https://github.com/dsherret/dax), and [bnx](https://github.com/wobsoriano/bnx). Thank you to the authors of those projects.