| # 🗄️ modern-tar |
| |
| Zero-dependency, cross-platform, streaming tar archive library for every JavaScript runtime. Built with the browser-native Web Streams API for performance and memory efficiency. |
| |
| ## Features |
| |
| - 🚀 **Streaming Architecture** - Supports large archives without loading everything into memory. |
| - 📋 **Standards Compliant** - Full USTAR format support with PAX extensions. Compatible with GNU tar, BSD tar, and other standard implementations. |
| - 🗜️ **Compression** - Includes helpers for gzip compression/decompression. |
| - 📝 **TypeScript First** - Full type safety with detailed TypeDoc documentation. |
| - ⚡ **Zero Dependencies** - No external dependencies, minimal bundle size. |
| - 🌐 **Cross-Platform** - Works in browsers, Node.js, Cloudflare Workers, and other JavaScript runtimes. |
| - 📁 **Node.js Integration** - Additional high-level APIs for directory packing and extraction. |
| |
| ## Installation |
| |
| ```sh |
| npm install modern-tar |
| ``` |
| |
| ## Usage |
| |
| This package provides two entry points: |
| |
| - `modern-tar`: The core, cross-platform streaming API (works everywhere). |
| - `modern-tar/fs`: High-level filesystem utilities for Node.js. |
| |
| ### Core Usage |
| |
| These APIs use the Web Streams API and can be used in any modern JavaScript environment. |
| |
| #### Simple |
| |
| ```typescript |
| import { packTar, unpackTar } from 'modern-tar'; |
| |
| // Pack entries into a tar buffer |
| const entries = [ |
| { header: { name: "file.txt", size: 5 }, body: "hello" }, |
| { header: { name: "dir/", type: "directory", size: 0 } }, |
| { header: { name: "dir/nested.txt", size: 3 }, body: new Uint8Array([97, 98, 99]) } // "abc" |
| ]; |
| |
| // Accepts string, Uint8Array, Blob, ReadableStream<Uint8Array> and more... |
| const tarBuffer = await packTar(entries); |
| |
| // Unpack tar buffer into entries |
| const entries = await unpackTar(tarBuffer); |
| for (const entry of entries) { |
| console.log(`File: ${entry.header.name}`); |
| const content = new TextDecoder().decode(entry.data); |
| console.log(`Content: ${content}`); |
| } |
| ``` |
| |
| #### Streaming |
| |
| ```typescript |
| import { createTarPacker, createTarDecoder } from 'modern-tar'; |
| |
| // Create a tar packer |
| const { readable, controller } = createTarPacker(); |
| |
| // Add entries dynamically |
| const fileStream = controller.add({ |
| name: "dynamic.txt", |
| size: 5, |
| type: "file" |
| }); |
| |
| // Write content to the stream |
| const writer = fileStream.getWriter(); |
| await writer.write(new TextEncoder().encode("hello")); |
| await writer.close(); |
| |
| // When done adding entries, finalize the archive |
| controller.finalize(); |
| |
| // Pipe the archive right into a decoder |
| const decodedStream = readable.pipeThrough(createTarDecoder()); |
| for await (const entry of decodedStream) { |
| console.log(`Decoded: ${entry.header.name}`); |
| |
| const shouldSkip = entry.header.name.endsWith(".md"); |
| if (shouldSkip) { |
| // You MUST drain the body with cancel() to proceed to the next entry or read it fully, |
| // otherwise the stream will stall. |
| await entry.body.cancel(); |
| continue; |
| } |
| |
| const reader = entry.body.getReader(); |
| while (true) { |
| const { done, value } = await reader.read(); |
| if (done) break; |
| processChunk(value); |
| } |
| } |
| ``` |
| |
| #### Compression/Decompression (gzip) |
| |
| ```typescript |
| import { createGzipEncoder, createTarPacker } from 'modern-tar'; |
| |
| // Create and compress a tar archive |
| const { readable, controller } = createTarPacker(); |
| const compressedStream = readable.pipeThrough(createGzipEncoder()); |
| |
| // Add entries... |
| const fileStream = controller.add({ name: "file.txt", size: 5, type: "file" }); |
| const writer = fileStream.getWriter(); |
| await writer.write(new TextEncoder().encode("hello")); |
| await writer.close(); |
| controller.finalize(); |
| |
| // Upload compressed .tar.gz |
| await fetch('/api/upload', { |
| method: 'POST', |
| body: compressedStream, |
| headers: { 'Content-Type': 'application/gzip' } |
| }); |
| ``` |
| |
| ```typescript |
| import { createGzipDecoder, createTarDecoder, unpackTar } from 'modern-tar'; |
| |
| // Download and process a .tar.gz file |
| const response = await fetch('https://api.example.com/archive.tar.gz'); |
| if (!response.body) throw new Error('No response body'); |
| |
| // Buffer entire archive |
| const entries = await unpackTar(response.body.pipeThrough(createGzipDecoder())); |
| |
| for (const entry of entries) { |
| console.log(`Extracted: ${entry.header.name}`); |
| const content = new TextDecoder().decode(entry.data); |
| console.log(`Content: ${content}`); |
| } |
| |
| // Or chain decompression and tar parsing using streams |
| const entries = response.body |
| .pipeThrough(createGzipDecoder()) |
| .pipeThrough(createTarDecoder()); |
| |
| for await (const entry of entries) { |
| console.log(`Extracted: ${entry.header.name}`); |
| // Process entry.body ReadableStream as needed |
| } |
| ``` |
| |
| ### Node.js Filesystem Usage |
| |
| These APIs use Node.js streams when interacting with the local filesystem. |
| |
| #### Simple |
| |
| ```typescript |
| import { packTar, unpackTar } from 'modern-tar/fs'; |
| import { createWriteStream, createReadStream } from 'node:fs'; |
| import { pipeline } from 'node:stream/promises'; |
| |
| // Pack a directory into a tar file |
| const tarStream = packTar('./my/project'); |
| const fileStream = createWriteStream('./project.tar'); |
| await pipeline(tarStream, fileStream); |
| |
| // Extract a tar file to a directory |
| const tarReadStream = createReadStream('./project.tar', { |
| highWaterMark: 256 * 1024 // 256 KB for optimal performance |
| }); |
| const extractStream = unpackTar('./output/directory'); |
| await pipeline(tarReadStream, extractStream); |
| ``` |
| |
| #### Filtering and Transformation |
| |
| ```typescript |
| import { packTar, unpackTar } from 'modern-tar/fs'; |
| import { createReadStream } from 'node:fs'; |
| import { pipeline } from 'node:stream/promises'; |
| |
| // Pack with filtering |
| const packStream = packTar('./my/project', { |
| filter: (filePath, stats) => !filePath.includes('node_modules'), |
| map: (header) => ({ ...header, mode: 0o644 }), // Set all files to 644 |
| dereference: true // Follow symlinks instead of archiving them |
| }); |
| |
| // Unpack with advanced options |
| const sourceStream = createReadStream('./archive.tar', { |
| highWaterMark: 256 * 1024 // 256 KB for optimal performance |
| }); |
| const extractStream = unpackTar('./output', { |
| // Core options |
| strip: 1, // Remove first directory level |
| filter: (header) => header.name.endsWith('.js'), // Only extract JS files |
| map: (header) => ({ ...header, name: header.name.toLowerCase() }), // Transform names |
| |
| // Filesystem-specific options |
| fmode: 0o644, // Override file permissions |
| dmode: 0o755, // Override directory permissions |
| maxDepth: 50, // Limit extraction depth for security (default: 1024) |
| concurrency: 8 // Limit concurrent filesystem operations (default: CPU cores) |
| }); |
| |
| await pipeline(sourceStream, extractStream); |
| ``` |
| |
| #### Archive Creation |
| |
| ```typescript |
| import { packTar, type TarSource } from 'modern-tar/fs'; |
| import { createWriteStream } from 'node:fs'; |
| import { pipeline } from 'node:stream/promises'; |
| |
| // Pack multiple sources |
| const sources: TarSource[] = [ |
| { type: 'file', source: './package.json', target: 'project/package.json' }, |
| { type: 'directory', source: './src', target: 'project/src' }, |
| { type: 'content', content: 'Hello World!', target: 'project/hello.txt' }, |
| { type: 'content', content: '#!/bin/bash\necho "Executable"', target: 'bin/script.sh', mode: 0o755 }, |
| { type: 'stream', content: createReadStream('./large-file.bin'), target: 'project/data.bin', size: 1048576 }, |
| { type: 'stream', content: fetch('/api/data').then(r => r.body!), target: 'project/remote.json', size: 2048 } |
| ]; |
| |
| const archiveStream = packTar(sources); |
| await pipeline(archiveStream, createWriteStream('project.tar')); |
| ``` |
| |
| #### Compression/Decompression (gzip) |
| |
| ```typescript |
| import { packTar, unpackTar } from 'modern-tar/fs'; |
| import { createWriteStream, createReadStream } from 'node:fs'; |
| import { createGzip, createGunzip } from 'node:zlib'; |
| import { pipeline } from 'node:stream/promises'; |
| |
| // Pack directory and compress to .tar.gz |
| const tarStream = packTar('./my/project'); |
| await pipeline(tarStream, createGzip(), createWriteStream('./project.tar.gz')); |
| |
| // Decompress and extract .tar.gz |
| const gzipStream = createReadStream('./project.tar.gz', { |
| highWaterMark: 256 * 1024 // 256 KB for optimal performance |
| }); |
| await pipeline(gzipStream, createGunzip(), unpackTar('./output')); |
| ``` |
| |
| ## API Reference |
| |
| See the [API Reference](./REFERENCE.md). |
| |
| # Benchmarks |
| |
| Current benchmarks indicate we're much faster than other popular tar libraries for small file archives (packing and unpacking). On the other hand, larger files hit an I/O bottleneck resulting in similar performance between libraries. |
| |
| See the [Results](./benchmarks/README.md). |
| |
| ## Compatibility |
| |
| The core library uses the [Web Streams API](https://caniuse.com/streams) and requires: |
| |
| - **Node.js**: 18.0+ |
| - **Browsers**: Modern browsers with Web Streams support |
| - Chrome 71+ |
| - Firefox 102+ |
| - Safari 14.1+ |
| - Edge 79+ |
| |
| ## Acknowledgements |
| |
| - [`tar-stream`](https://github.com/mafintosh/tar-stream) and [`tar-fs`](https://github.com/mafintosh/tar-fs) - For the inspiration and test fixtures. |
| |
| ## License |
| |
| MIT |