JSPM

  • Created
  • Published
  • Downloads 195460
  • Score
    100M100P100Q169064F
  • License MIT

Zip/Tar a complete folder or a glob list into a zip/tgz file

Package Exports

  • zip-a-folder

Readme

NPM

CircleCI Codacy Badge Codacy Coverage Known Vulnerabilities

zip-a-folder

A fast, dependency-free ZIP/TAR/TGZ creation library using native Node.js compression (zlib), supporting:

  • ZIP archives (with optional ZIP64)
  • TAR archives (optionally gzipped)
  • Globs (single or comma-separated)
  • Parallel directory scanning (statConcurrency)
  • Custom write streams
  • Compression presets (high, medium, uncompressed)
  • Fine-grained zlib/gzip control

Everything is implemented natively without JS zip/tar libraries.


⚠️ Incompatible Changes

Version 2

Added support for comma-separated glob lists.
This may change the behavior for cases previously interpreted as “folder only”.

Version 3

Dual-module support (CJS + ESM).

Version 3.1

Added support for destPath to control the internal path layout of created archives.

Version 4 (current)

A major rewrite using:

  • Fully native ZIP writer (no dependencies)
  • Native TAR + gzip writer
  • ZIP64 support for large archives
  • Parallel statting (statConcurrency)
  • Strict internal path normalization mirroring classic zip-a-folder behavior
  • Native glob handling via glob

📦 Installation

npm install zip-a-folder

🚀 Usage

Create a ZIP file

import { zip } from 'zip-a-folder';

await zip('/path/to/folder', '/path/to/archive.zip');

Create a GZIP-compressed TAR file (.tgz)

import { tar } from 'zip-a-folder';

await tar('/path/to/folder', '/path/to/archive.tgz');

🎚 Compression Handling

Supported compression levels:

COMPRESSION_LEVEL.high          // highest compression (default)
COMPRESSION_LEVEL.medium        // balanced
COMPRESSION_LEVEL.uncompressed  // STORE for zip, no-gzip for tar

Example:

import { zip, COMPRESSION_LEVEL } from 'zip-a-folder';

await zip('/path/to/folder', '/path/to/archive.zip', {
    compression: COMPRESSION_LEVEL.medium
});

✨ ZIP Options

Option Type Description
comment string ZIP file comment
forceLocalTime boolean Use local timestamps instead of UTC
forceZip64 boolean Always include ZIP64 headers
namePrependSlash boolean Prefix all ZIP entry names with /
store boolean Force STORE method (no compression)
zlib ZlibOptions Passed directly to zlib.deflateRaw
statConcurrency number Parallel stat workers (default: 4)
destPath string Prefix inside the archive (>=3.1)
customWriteStream WriteStream Manually handle output

Example:

await zip('/dir', '/archive.zip', {
    comment: "Created by zip-a-folder",
    forceZip64: true,
    namePrependSlash: true,
    store: false,
    statConcurrency: 16,
    zlib: { level: 9 }
});

📦 TAR / TGZ Options

Option Type Description
gzip boolean Enable gzip compression
gzipOptions ZlibOptions Passed to zlib.createGzip
statConcurrency number Parallel stat workers

Example:

await tar('/dir', '/archive.tgz', {
    gzip: true,
    gzipOptions: { level: 6 }
});

🔧 Custom Write Streams

ZIP and TAR can be written into any stream. If customWriteStream is used, the targetFilePath can be empty or undefined.

import fs from 'fs';
import { zip } from 'zip-a-folder';

const ws = fs.createWriteStream('/tmp/output.zip');
await zip('/path/to/folder', undefined, { customWriteStream: ws });

Important: zip-a-folder does not validate custom streams. You must ensure:

  • parent directory exists
  • you’re not writing into the source directory (to avoid recursion)

🔍 Glob Handling

The first parameter may be:

  • A path to a directory
  • A single glob
  • A comma-separated list of globs

Example:

await zip('**/*.json', '/archive.zip');
await zip('**/*.json, **/*.txt', '/archive2.zip');

If no files match, zip-a-folder throws:

Error: No glob match found

🗂 Destination Path Handling (destPath)

Adds a prefix inside the archive:

await zip('data/', '/archive.zip', { destPath: 'data/' });

Resulting ZIP layout:

data/file1.txt
data/subdir/file2.txt

Directory Root Inclusion Semantics

When passing a directory path as the first argument (e.g. zip('/path/to/folder', '/archive.zip')), the archive by default contains the contents of that directory at the archive root (i.e. you will see the files inside folder/, not a top-level folder/ directory itself).

Include the directory itself

If you want the archive to unpack into the named folder (so the top level of the archive contains folder/), set destPath to that folder name plus a trailing slash:

await zip('/path/to/folder', '/archive.zip', {
  destPath: 'folder/'
});

Result layout:

folder/file1.txt
folder/sub/file2.txt

Summary

  • Default: directory contents only (no enclosing folder)
  • To include the folder: use destPath: '<dirname>/'

This applies equally to tar().


🎯 Native Implementation Notes (New in v4)

  • ZIP and TAR are written using pure Node.js (zlib, raw buffering)
  • ZIP64 support included
  • File system scanning performed with a parallel stat queue
  • Globs handled via the standardized glob package
  • Archive layout matches the original zip-a-folder for compatibility
  • ZIP writer supports dependency-free deflate and manual header construction
  • TAR writer produces POSIX ustar format with proper 512-byte block alignment

🧪 Running Tests

Tests are written in Jest:

npm test

A coverage report is included:

npm test -- --coverage

❤️ Thanks

Special thanks to contributors:

  • @sole – initial work
  • @YOONBYEONGIN
  • @Wunschik
  • @ratbeard
  • @Xotabu4
  • @dallenbaldwin
  • @wiralegawa
  • @karan-gaur
  • @malthe

Additional thanks to everyone helping shape the native rewrite.