Skip to content

Repository files navigation

πŸ—„οΈ 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 - Supports gzip compression and decompression with native compression streams.
  • πŸ“ 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

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

import { packTar, unpackTar } from 'modern-tar';

// Entry bodies can be strings, bytes, Blobs, or ReadableStreams.
const archive = await packTar([
	{ 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"
]);

// Unpack tar buffer into entries
const extracted = await unpackTar(archive);
const decoder = new TextDecoder();

for (const { header, data } of extracted) {
	if (data) {
		console.log(`${header.name}: ${decoder.decode(data)}`);
	} else {
		console.log(`${header.type}: ${header.name}`);
	}
}

Streaming

import { createTarDecoder } from 'modern-tar';

const response = await fetch('/archive.tar');
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
if (!response.body) throw new Error('No response body');

const entries = response.body.pipeThrough(createTarDecoder());
for await (const entry of entries) {
	if (!entry.header.name.endsWith(".txt")) {
		// You MUST read each entry body completely before advancing.
		// If an entry is not needed, cancel its body so the stream does not stall.
		await entry.body.cancel();
		continue;
	}

	const text = await new Response(entry.body).text();
	console.log(`${entry.header.name}: ${text}`);
}

Compression/Decompression (gzip)

import { createTarPacker } from 'modern-tar';

const { readable, controller } = createTarPacker();
const gzipResponse = new Response(
  readable.pipeThrough(new CompressionStream('gzip')),
  { headers: { 'Content-Type': 'application/gzip' } }
);

const body = controller.add({ name: 'hello.txt', size: 5 });
const writer = body.getWriter();
await writer.write(new TextEncoder().encode('hello'));
await writer.close();
controller.finalize();

// Return gzipResponse from your server or Worker handler.
import { unpackTar } from 'modern-tar';

// Download and unpack a .tar.gz without buffering the compressed archive
const response = await fetch('https://api.example.com/archive.tar.gz');
if (!response.ok) throw new Error(`Download failed: ${response.status}`);
if (!response.body) throw new Error('No response body');

const entries = await unpackTar(
  response.body.pipeThrough(new DecompressionStream('gzip'))
);
const decoder = new TextDecoder();

for (const { header, data } of entries) {
  if (data) {
    console.log(`${header.name}: ${decoder.decode(data)}`);
  }
}

Node.js Filesystem Usage

These APIs use Node.js streams when interacting with the local filesystem.

Simple

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

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

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)

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.

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.

Compatibility

The core library uses the Web Streams API and requires:

  • Node.js: 18.0+
  • TypeScript: 5.7+ when consuming the bundled declarations
  • Browsers: Baseline Widely available
    • Chrome 85+
    • Edge 85+
    • Firefox 102+
    • Safari 14.1+

If you use CompressionStream or DecompressionStream, Firefox 113+ and Safari 16.4+ are required.

Acknowledgements

License

MIT

About

πŸ—„ Zero dependency streaming tar parser and writer for every JavaScript runtime.

Topics

Resources

Stars

Watchers

Forks

Releases

Sponsor this project

Used by

Contributors

Languages