feat(archive): add TypeScript types, docs, and files() benchmark (#25922)

## Summary

- Add comprehensive TypeScript type definitions for `Bun.Archive` in
`bun.d.ts`
  - `ArchiveInput` and `ArchiveCompression` types
- Full JSDoc documentation with examples for all methods (`from`,
`write`, `extract`, `blob`, `bytes`, `files`)
- Add documentation page at `docs/runtime/archive.mdx`
  - Quickstart examples
  - Creating and extracting archives
  - `files()` method with glob filtering
  - Compression support
  - Full API reference section
- Add Archive to docs sidebar under "Data & Storage"
- Add `files()` benchmark comparing `Bun.Archive.files()` vs node-tar
- Shows ~7x speedup for reading archive contents into memory (59µs vs
434µs)

## Test plan

- [x] TypeScript types compile correctly
- [x] Documentation renders properly in Mintlify format
- [x] Benchmark runs successfully and shows performance comparison
- [x] Verified `files()` method works correctly with both Bun.Archive
and node-tar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
This commit is contained in:
robobun
2026-01-09 19:00:19 -08:00
committed by GitHub
parent d4a966f8ae
commit 7076a49bb1
7 changed files with 1295 additions and 21 deletions

View File

@@ -6965,6 +6965,296 @@ declare module "bun" {
match(str: string): boolean;
}
/**
* Input data for creating an archive. Can be:
* - An object mapping paths to file contents (string, Blob, TypedArray, or ArrayBuffer)
* - A Blob containing existing archive data
* - A TypedArray or ArrayBuffer containing existing archive data
*/
type ArchiveInput = Record<string, BlobPart> | Blob | ArrayBufferView | ArrayBufferLike;
/**
* Compression format for archive output.
* - `"gzip"` - Compress with gzip
* - `true` - Same as `"gzip"`
* - `false` - Explicitly disable compression (no compression)
* - `undefined` - No compression (default behavior when omitted)
*
* Both `false` and `undefined` result in no compression; `false` can be used
* to explicitly indicate "no compression" in code where the intent should be clear.
*/
type ArchiveCompression = "gzip" | boolean;
/**
* Options for extracting archive contents.
*/
interface ArchiveExtractOptions {
/**
* Glob pattern(s) to filter which entries are extracted.
* Uses the same syntax as {@link Bun.Glob}, including support for wildcards (`*`, `**`),
* character classes (`[abc]`), alternation (`{a,b}`), and negation (`!pattern`).
*
* Patterns are matched against archive entry paths normalized to use forward slashes (`/`),
* regardless of the host operating system. Always write patterns using `/` as the separator.
*
* - Positive patterns: Only entries matching at least one pattern will be extracted.
* - Negative patterns (prefixed with `!`): Entries matching these patterns will be excluded.
* Negative patterns are applied after positive patterns.
*
* If not specified, all entries are extracted.
*
* @example
* ```ts
* // Extract only TypeScript files
* await archive.extract("./out", { glob: "**" + "/*.ts" });
*
* // Extract files from multiple directories
* await archive.extract("./out", { glob: ["src/**", "lib/**"] });
*
* // Exclude node_modules using negative pattern
* await archive.extract("./out", { glob: ["**", "!node_modules/**"] });
*
* // Extract source files but exclude tests
* await archive.extract("./out", { glob: ["src/**", "!**" + "/*.test.ts"] });
* ```
*/
glob?: string | readonly string[];
}
/**
* A class for creating and extracting tar archives with optional gzip compression.
*
* `Bun.Archive` provides a fast, native implementation for working with tar archives.
* It supports creating archives from in-memory data or extracting existing archives
* to disk or memory.
*
* @example
* **Create an archive from an object:**
* ```ts
* const archive = Bun.Archive.from({
* "hello.txt": "Hello, World!",
* "data.json": JSON.stringify({ foo: "bar" }),
* "binary.bin": new Uint8Array([1, 2, 3, 4]),
* });
* ```
*
* @example
* **Extract an archive to disk:**
* ```ts
* const archive = Bun.Archive.from(tarballBytes);
* const entryCount = await archive.extract("./output");
* console.log(`Extracted ${entryCount} entries`);
* ```
*
* @example
* **Get archive contents as a Map of File objects:**
* ```ts
* const archive = Bun.Archive.from(tarballBytes);
* const entries = await archive.files();
* for (const [path, file] of entries) {
* console.log(path, await file.text());
* }
* ```
*
* @example
* **Write a gzipped archive directly to disk:**
* ```ts
* await Bun.Archive.write("bundle.tar.gz", {
* "src/index.ts": sourceCode,
* "package.json": packageJson,
* }, "gzip");
* ```
*/
export class Archive {
/**
* Create an `Archive` instance from input data.
*
* @param data - The input data for the archive:
* - **Object**: Creates a new tarball with the object's keys as file paths and values as file contents
* - **Blob/TypedArray/ArrayBuffer**: Wraps existing archive data (tar or tar.gz)
*
* @returns A new `Archive` instance
*
* @example
* **From an object (creates new tarball):**
* ```ts
* const archive = Bun.Archive.from({
* "hello.txt": "Hello, World!",
* "nested/file.txt": "Nested content",
* });
* ```
*
* @example
* **From existing archive data:**
* ```ts
* const response = await fetch("https://example.com/package.tar.gz");
* const archive = Bun.Archive.from(await response.blob());
* ```
*/
static from(data: ArchiveInput): Archive;
/**
* Create and write an archive directly to disk in one operation.
*
* This is more efficient than creating an archive and then writing it separately,
* as it streams the data directly to disk.
*
* @param path - The file path to write the archive to
* @param data - The input data for the archive (same as `Archive.from()`)
* @param compress - Optional compression: `"gzip"`, `true` for gzip, or `false`/`undefined` for none
*
* @returns A promise that resolves when the write is complete
*
* @example
* **Write uncompressed tarball:**
* ```ts
* await Bun.Archive.write("output.tar", {
* "file1.txt": "content1",
* "file2.txt": "content2",
* });
* ```
*
* @example
* **Write gzipped tarball:**
* ```ts
* await Bun.Archive.write("output.tar.gz", files, "gzip");
* ```
*/
static write(path: string, data: ArchiveInput | Archive, compress?: ArchiveCompression): Promise<void>;
/**
* Extract the archive contents to a directory on disk.
*
* Creates the target directory and any necessary parent directories if they don't exist.
* Existing files will be overwritten.
*
* @param path - The directory path to extract to
* @param options - Optional extraction options
* @param options.glob - Glob pattern(s) to filter entries (positive patterns include, negative patterns starting with `!` exclude)
* @returns A promise that resolves with the number of entries extracted (files, directories, and symlinks)
*
* @example
* **Extract all entries:**
* ```ts
* const archive = Bun.Archive.from(tarballBytes);
* const count = await archive.extract("./extracted");
* console.log(`Extracted ${count} entries`);
* ```
*
* @example
* **Extract only TypeScript files:**
* ```ts
* const count = await archive.extract("./src", { glob: "**" + "/*.ts" });
* ```
*
* @example
* **Extract everything except tests:**
* ```ts
* const count = await archive.extract("./dist", { glob: ["**", "!**" + "/*.test.*"] });
* ```
*
* @example
* **Extract source files but exclude tests:**
* ```ts
* const count = await archive.extract("./output", {
* glob: ["src/**", "lib/**", "!**" + "/*.test.ts", "!**" + "/__tests__/**"]
* });
* ```
*/
extract(path: string, options?: ArchiveExtractOptions): Promise<number>;
/**
* Get the archive contents as a `Blob`.
*
* @param compress - Optional compression: `"gzip"`, `true` for gzip, or `false`/`undefined` for none
* @returns A promise that resolves with the archive data as a Blob
*
* @example
* **Get uncompressed tarball:**
* ```ts
* const blob = await archive.blob();
* ```
*
* @example
* **Get gzipped tarball:**
* ```ts
* const gzippedBlob = await archive.blob("gzip");
* ```
*/
blob(compress?: ArchiveCompression): Promise<Blob>;
/**
* Get the archive contents as a `Uint8Array`.
*
* @param compress - Optional compression: `"gzip"`, `true` for gzip, or `false`/`undefined` for none
* @returns A promise that resolves with the archive data as a Uint8Array
*
* @example
* **Get uncompressed tarball bytes:**
* ```ts
* const bytes = await archive.bytes();
* ```
*
* @example
* **Get gzipped tarball bytes:**
* ```ts
* const gzippedBytes = await archive.bytes("gzip");
* ```
*/
bytes(compress?: ArchiveCompression): Promise<Uint8Array<ArrayBuffer>>;
/**
* Get the archive contents as a `Map` of `File` objects.
*
* Each file in the archive is returned as a `File` object with:
* - `name`: The file path within the archive
* - `lastModified`: The file's modification time from the archive
* - Standard Blob methods (`text()`, `arrayBuffer()`, `stream()`, etc.)
*
* Only regular files are included; directories are not returned.
* File contents are loaded into memory, so for large archives consider using `extract()` instead.
*
* @param glob - Optional glob pattern(s) to filter files. Supports the same syntax as {@link Bun.Glob},
* including negation patterns (prefixed with `!`). Patterns are matched against paths normalized
* to use forward slashes (`/`).
* @returns A promise that resolves with a Map where keys are file paths (always using forward slashes `/` as separators) and values are File objects
*
* @example
* **Get all files:**
* ```ts
* const entries = await archive.files();
* for (const [path, file] of entries) {
* console.log(`${path}: ${file.size} bytes`);
* }
* ```
*
* @example
* **Filter by glob pattern:**
* ```ts
* const tsFiles = await archive.files("**" + "/*.ts");
* const srcFiles = await archive.files(["src/**", "lib/**"]);
* ```
*
* @example
* **Exclude files with negative patterns:**
* ```ts
* // Get all source files except tests
* const srcFiles = await archive.files(["src/**", "!**" + "/*.test.ts"]);
* ```
*
* @example
* **Read file contents:**
* ```ts
* const entries = await archive.files();
* const readme = entries.get("README.md");
* if (readme) {
* console.log(await readme.text());
* }
* ```
*/
files(glob?: string | readonly string[]): Promise<Map<string, File>>;
}
/**
* Generate a UUIDv7, which is a sequential ID based on the current timestamp with a random component.
*