A TypeScript NPM package that zips files and folders into a JSON format, enabling bundling for Bun and other JavaScript runtimes. The package provides both CLI and programmatic interfaces for zipping, unzipping, and listing archive contents.
- Use Node.js built-in
zlibmodule for compression (gzip) - Store files as base64-encoded strings in a JSON structure
- Compress the entire file collection as a single blob for efficiency
- Include metadata for each file (path, size, permissions, timestamps)
{
"meta": {
"version": "1.0.0",
"createdAt": "ISO-8601 timestamp",
"files": [
{
"path": "relative/path/to/file.txt",
"size": 1024,
"mode": 33188, // Unix file permissions
"isDirectory": false,
"modifiedAt": "ISO-8601 timestamp"
}
],
"totalSize": 10240, // Total uncompressed size
"fileCount": 5 // Number of files (excluding directories)
},
"blob": "base64-encoded-gzipped-content"
}zip-json/
├── src/ # TypeScript source files
│ ├── index.ts # Main entry point & exports
│ ├── cli.ts # CLI implementation
│ ├── core/ # Core functionality
│ │ ├── compressor.ts # Compression/decompression logic
│ │ ├── archiver.ts # Archive creation logic
│ │ ├── extractor.ts # Archive extraction logic
│ │ └── types.ts # TypeScript interfaces
│ └── utils/ # Utility functions
│ ├── file.ts # File system operations
│ ├── glob.ts # Glob pattern handling
│ └── format.ts # Output formatting
├── bin/ # Executable scripts (generated)
│ └── zip-json.js # CLI executable
├── dist/ # Compiled output (generated)
│ ├── esm/ # ES modules
│ ├── cjs/ # CommonJS modules
│ └── types/ # TypeScript declarations
├── docs/ # Documentation
│ ├── api.md # API reference
│ ├── cli.md # CLI documentation
│ └── types.md # TypeScript types reference
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── fixtures/ # Test files
├── scripts/
│ └── build.ts # Build script
├── package.json
├── tsconfig.json
├── tsconfig.esm.json
├── tsconfig.cjs.json
├── biome.json
├── bunfig.toml
├── .gitignore
├── README.md
├── LICENSE.md
└── spec.md # This file
{
"name": "zip-json",
"version": "1.0.0",
"description": "Zip files and folders into a JSON format for bundling and runtime extraction",
"keywords": ["zip", "json", "bundle", "compress", "archive", "bun"],
"author": "Your Name",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/yourusername/zip-json.git"
},
"type": "module",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
"types": "./dist/types/index.d.ts",
"exports": {
".": {
"types": "./dist/types/index.d.ts",
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"bin": {
"zip-json": "./bin/zip-json.js"
},
"files": [
"dist",
"bin",
"README.md",
"LICENSE.md"
],
"scripts": {
"build": "bun run scripts/build.ts",
"build:watch": "bun run scripts/build.ts --watch",
"test": "bun test",
"test:coverage": "bun test --coverage",
"test:watch": "bun test --watch",
"lint": "biome check --apply ./src",
"format": "biome format --write ./src ./tests",
"check": "biome check ./src ./tests",
"prepublishOnly": "bun run build && bun run test:coverage",
"postbuild": "chmod +x bin/zip-json.js"
},
"dependencies": {
"commander": "^12.0.0",
"glob": "^10.3.10",
"chalk": "^5.3.0"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.0.0",
"typescript": "^5.3.0",
"@biomejs/biome": "^1.5.0"
},
"engines": {
"node": ">=22.0.0"
},
"engineStrict": true
}- Enable organize imports
- Set recommended linting rules
- Configure formatting: 2 spaces, single quotes, semicolons, trailing commas
- Line width: 100 characters
- Ignore: node_modules, dist, bin, coverage, *.json
- Target: ES2022
- Module: ESNext with bundler resolution
- Strict mode enabled
- Generate declarations, source maps
- Output: dist/types for declarations
function build():
clean dist and bin directories
create directory structure
// Build TypeScript declarations
run tsc --emitDeclarationOnly
// Build ESM version
Bun.build({format: 'esm', outdir: 'dist/esm'})
// Build CJS version
Bun.build({format: 'cjs', outdir: 'dist/cjs'})
// Build CLI executable
Bun.build({format: 'esm', outdir: 'bin', minify: true})
// Add shebang to CLI
prepend #!/usr/bin/env node to bin/zip-json.js
make executable
if --watch flag:
watch src directory for changes
rebuild on .ts file changes
interface ZipJsonData {
meta: {
version: string
createdAt: string
files: FileEntry[]
totalSize: number
fileCount: number
}
blob: string // Base64 encoded compressed data
}
interface FileEntry {
path: string
size: number
mode: number
isDirectory: boolean
modifiedAt: string
}
interface ZipOptions {
baseDir?: string
ignore?: string[]
followSymlinks?: boolean
onProgress?: ProgressCallback
}
interface UnzipOptions {
outputDir?: string
overwrite?: boolean
preservePermissions?: boolean
onProgress?: ProgressCallback
}
interface ProgressInfo {
type: 'zip' | 'unzip'
currentFile: string
processedFiles: number
totalFiles: number
processedBytes: number
totalBytes: number
percentage: number
}
type ProgressCallback = (progress: ProgressInfo) => voidclass Compressor:
async compress(data: string) -> string:
buffer = Buffer.from(data, 'utf-8')
compressed = await gzip(buffer, {level: 9})
return compressed.toString('base64')
async decompress(base64Data: string) -> string:
buffer = Buffer.from(base64Data, 'base64')
decompressed = await gunzip(buffer)
return decompressed.toString('utf-8')
class Archiver:
async archive(patterns, options):
// Collect files using glob
files = await collectFiles(patterns, options)
fileEntries = []
fileContents = {}
totalSize = 0
for each file in files:
// Get file stats
entry = createFileEntry(file)
fileEntries.push(entry)
if not directory:
// Read and encode file
content = await readFile(file)
fileContents[entry.path] = base64(content)
totalSize += entry.size
// Report progress
if options.onProgress:
options.onProgress(progressInfo)
// Compress all contents together
jsonString = JSON.stringify(fileContents)
blob = await compressor.compress(jsonString)
return {
meta: {version, createdAt, files, totalSize, fileCount},
blob: blob
}
private async collectFiles(patterns, options):
// Use glob to find all matching files
// Handle ignore patterns
// Follow symlinks if specified
return matchedFiles
class Extractor:
async extract(data, options):
outputDir = options.outputDir || cwd
// Decompress blob
decompressed = await compressor.decompress(data.blob)
fileContents = JSON.parse(decompressed)
extractedFiles = []
for each entry in data.meta.files:
outputPath = join(outputDir, entry.path)
if entry.isDirectory:
await mkdir(outputPath, recursive)
else:
// Check overwrite
if exists(outputPath) and not options.overwrite:
throw Error("File exists")
// Ensure directory exists
await mkdir(dirname(outputPath), recursive)
// Write file
content = fileContents[entry.path]
buffer = Buffer.from(content, 'base64')
await writeFile(outputPath, buffer)
// Preserve permissions if requested
if options.preservePermissions:
await chmod(outputPath, entry.mode)
extractedFiles.push(outputPath)
// Report progress
if options.onProgress:
options.onProgress(progressInfo)
return extractedFiles
class ZipJson:
archiver = new Archiver()
extractor = new Extractor()
async zip(patterns, options):
return archiver.archive(patterns, options)
async zipToFile(patterns, outputPath, options):
data = await zip(patterns, options)
await writeFile(outputPath, JSON.stringify(data))
async unzip(data, options):
return extractor.extract(data, options)
async unzipFromFile(inputPath, options):
data = JSON.parse(await readFile(inputPath))
return unzip(data, options)
list(data):
return data.meta.files
async listFromFile(inputPath):
data = JSON.parse(await readFile(inputPath))
return list(data)
// Export functional API
export const zip = (patterns, options) => new ZipJson().zip(patterns, options)
export const unzip = (data, options) => new ZipJson().unzip(data, options)
export const list = (data) => new ZipJson().list(data)
#!/usr/bin/env node
program = new Command()
// Setup commands
program
.command('zip <o> [patterns...]')
.options(baseDir, ignore, followSymlinks, noProgress)
.action((output, patterns, options) => {
// Validate inputs
// Create progress callback if needed
// Call zipToFile with options
// Display results
})
program
.command('unzip <input>')
.options(outputDir, overwrite, preservePermissions, noProgress)
.action((input, options) => {
// Check file exists
// Create progress callback if needed
// Call unzipFromFile with options
// Display results
})
program
.command('list <input>')
.options(detailed, sortBy)
.action((input, options) => {
// Read and parse archive
// Sort files based on option
// Display formatted list
// Show summary statistics
})
program.parse()
formatBytes(bytes):
// Convert bytes to human-readable format
// Returns: "1.5 MB", "500 KB", etc.
formatDate(date):
// Format date for display
// Returns localized date string
# zip-json
[NPM Badge] [License Badge] [Build Badge] [Coverage Badge]
> Bundle files and folders into JSON for Bun binary builds with runtime extraction
## ✨ Features
- 🚀 **Binary bundling** - Embed assets directly into Bun executables
- 📦 **JSON format** - Import bundles as JavaScript modules
- 🎯 **TypeScript-first** - Full type safety and IntelliSense support
- 💾 **Efficient compression** - Gzip compression for minimal bundle size
- 🔄 **Progress tracking** - Real-time feedback for large operations
- 🛠️ **Dual interface** - CLI tool and programmatic API
- ⚡ **Fast extraction** - Optimized for runtime asset extraction
- 🔒 **Permission preservation** - Maintains file permissions across platforms
## 📦 Installation
### Global CLI Installation
```bash
npm install -g zip-json
# or
bun add -g zip-jsonnpm install zip-json
# or
bun add zip-jsonbunx zip-json zip assets.json public/**/* resources/**/*# Bundle all your runtime assets into a single JSON file
zip-json zip assets.json \
public/**/* \
templates/**/* \
config/**/*.json \
--base-dir .// main.ts
import { unzip } from 'zip-json';
import assetsBundle from './assets.json';
// Extract assets on first run
async function initializeAssets() {
const assetDir = './runtime-assets';
if (!fs.existsSync(assetDir)) {
console.log('Extracting bundled assets...');
await unzip(assetsBundle, {
outputDir: assetDir,
preservePermissions: true
});
console.log('Assets ready!');
}
}
// Initialize before starting your app
await initializeAssets();
// Your application code here...# Build a single executable with embedded assets
bun build --compile --minify --sourcemap \
--external zip-json \
--entrypoint main.ts \
--outfile myapp
# Your binary now contains everything!
./myapp# Bundle assets for binary inclusion
zip-json zip bundle.json assets/ config/ data/
# With glob patterns
zip-json zip app-resources.json "public/**/*" "!**/*.test.js"
# Exclude unnecessary files
zip-json zip dist.json dist/ \
--ignore "*.map,*.test.js,*.spec.ts"# Extract to current directory
zip-json unzip bundle.json
# Extract to specific location
zip-json unzip bundle.json --output-dir ./extracted# List files in bundle
zip-json list bundle.json
# Detailed view with file sizes
zip-json list bundle.json --detailedimport { zip } from 'zip-json';
// Create asset bundle during build process
const bundle = await zip([
'public/**/*',
'templates/**/*.hbs',
'locales/**/*.json'
], {
baseDir: process.cwd(),
ignore: ['*.test.*', '*.spec.*']
});
// Save for inclusion in binary
await fs.writeFile('assets.json', JSON.stringify(bundle));import ZipJson from 'zip-json';
import bundledAssets from './assets.json';
const zipper = new ZipJson();
// Extract bundled assets at runtime
async function setupRuntime() {
const files = await zipper.unzip(bundledAssets, {
outputDir: './runtime',
overwrite: false // Skip if already extracted
});
console.log(`Extracted ${files.length} files`);
return files;
}
// Run once at startup
await setupRuntime();import { zip, unzip } from 'zip-json';
// Zip with progress tracking
const bundle = await zip(['large-folder/**/*'], {
onProgress: (progress) => {
const percent = progress.percentage;
const current = progress.currentFile;
console.log(`Zipping: ${percent}% - ${current}`);
}
});
// Extract with progress tracking
await unzip(bundle, {
outputDir: './assets',
onProgress: (progress) => {
const percent = progress.percentage;
const current = progress.currentFile;
console.log(`Extracting: ${percent}% - ${current}`);
}
});Traditional binary builders struggle with dynamic assets that need to be accessed as files at runtime. zip-json solves this by:
- Bundling assets into JSON that can be imported directly into your JavaScript/TypeScript
- Extracting them at runtime to the filesystem when your application starts
- Maintaining a single binary that contains both your code and assets
This is perfect for:
- CLI tools that need templates, configs, or other resources
- Desktop applications built with Bun that need to bundle assets
- Serverless functions that need to include static files
- Portable applications that must be distributed as a single file
- API Reference - Complete API documentation
- CLI Reference - Detailed CLI usage
- Type Definitions - TypeScript types
- Node.js >= 22.0.0
- Bun >= 1.0.0
# Clone the repository
git clone https://github.com/yourusername/zip-json.git
cd zip-json
# Install dependencies
bun install
# Build the project
bun run build
# Run tests
bun testbun run build- Build all formatsbun run build:watch- Build with file watchingbun test- Run testsbun run test:coverage- Run tests with coveragebun run lint- Lint and fix codebun run format- Format code
MIT © [Your Name]
See LICENSE.md for details.
### Documentation Files
#### docs/api.md
```markdown
# API Reference
Complete API documentation for the zip-json package.
## Class: ZipJson
Main class for creating and extracting JSON archives.
### Constructor
```javascript
new ZipJson()
Creates an archive from files and folders.
Creates an archive and saves it to a file.
Extracts an archive to the file system.
Extracts an archive from a file.
Lists the contents of an archive.
Lists the contents of an archive file.
Quick function to create archives.
Quick function to extract archives.
Quick function to list archive contents.
See types.md for complete type definitions.
#### docs/cli.md
```markdown
# CLI Reference
Complete command-line interface documentation for zip-json.
## Commands
### zip
Create a JSON archive from files and folders.
```bash
zip-json zip <output> [patterns...] [options]
Options:
-b, --base-dir <dir>- Base directory for relative paths-i, --ignore <patterns>- Comma-separated patterns to ignore--follow-symlinks- Follow symbolic links--no-progress- Disable progress indicator
Extract files from a JSON archive.
zip-json unzip <input> [options]Options:
-o, --output-dir <dir>- Output directory (default: current)--overwrite- Overwrite existing files--preserve-permissions- Preserve file permissions (default: true)--no-progress- Disable progress indicator
List contents of a JSON archive.
zip-json list <input> [options]Options:
-d, --detailed- Show detailed information-s, --sort-by <field>- Sort by: name, size, or date
See README for usage examples.
#### docs/types.md
```markdown
# Type Definitions
Complete TypeScript type definitions for zip-json.
## Main Types
### ZipJsonData
```typescript
interface ZipJsonData {
meta: {
version: string;
createdAt: string;
files: FileEntry[];
totalSize: number;
fileCount: number;
};
blob: string;
}
interface FileEntry {
path: string;
size: number;
mode: number;
isDirectory: boolean;
modifiedAt: string;
}interface ZipOptions {
baseDir?: string;
ignore?: string[];
followSymlinks?: boolean;
onProgress?: ProgressCallback;
}interface UnzipOptions {
outputDir?: string;
overwrite?: boolean;
preservePermissions?: boolean;
onProgress?: ProgressCallback;
}interface ProgressInfo {
type: 'zip' | 'unzip';
currentFile: string;
processedFiles: number;
totalFiles: number;
processedBytes: number;
totalBytes: number;
percentage: number;
}type ProgressCallback = (progress: ProgressInfo) => void;```markdown
# MIT License
Copyright (c) 2024 [Your Name]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
tests/
├── unit/
│ ├── compressor.test.ts # Test compression/decompression
│ ├── archiver.test.ts # Test archive creation
│ ├── extractor.test.ts # Test extraction
│ └── utils.test.ts # Test utility functions
├── integration/
│ ├── cli.test.ts # Test CLI commands
│ ├── api.test.ts # Test programmatic API
│ └── scenarios.test.ts # Test real-world scenarios
└── fixtures/
├── sample-files/ # Test files
└── sample-archives/ # Pre-made test archives
- 100% code coverage for all modules
- Test edge cases:
- Empty patterns
- Non-existent files
- Large files (>100MB)
- Deep directory structures
- Special characters in filenames
- Permission preservation
- Overwrite scenarios
- Progress callbacks
describe("Compressor"):
test "compresses and decompresses data":
original = "test data"
compressed = await compressor.compress(original)
decompressed = await compressor.decompress(compressed)
expect(decompressed).toBe(original)
test "handles large data":
// Test with 10MB+ strings
test "handles empty input":
// Test edge cases
describe("CLI":
test "zip command creates valid archive":
// Run CLI command
// Verify output file
// Check archive structure
test "handles missing arguments":
// Test error handling
FileNotFoundError: Input file/pattern doesn't existPermissionError: No read/write permissionsInvalidArchiveError: Corrupted or invalid JSONOverwriteError: File exists and overwrite not specifiedCompressionError: Compression/decompression failure
All errors should include:
- Clear description of what went wrong
- The file/path that caused the error
- Suggestion for how to fix it
Example:
Error: File already exists: /path/to/file.txt
Use --overwrite flag to replace existing files.
-
Memory Management
- Stream processing for files > 10MB
- Chunk large archives during compression
- Clear buffers after use
-
Parallel Processing
- Process multiple files concurrently (up to 10)
- Use worker threads for CPU-intensive compression
-
Progress Reporting
- Update progress at most once per 100ms
- Batch small file updates
- Use single-line updates in CLI
- Time to zip 1000 small files (< 1KB each)
- Time to zip 10 large files (100MB each)
- Memory usage for 1GB total data
- Compression ratio achieved
- Run full test suite with 100% coverage
- Lint and format all code with Biome
- Build all output formats (ESM, CJS, types)
- Test CLI commands locally
- Test as dependency in sample project
- Verify
bunx zip-jsonworks correctly - Update README with examples and API docs
- Create/update LICENSE.md file
- Set version in package.json
- Create git tag for version
- Run
npm publish - Test installation from NPM registry
- Verify TypeScript types work correctly
- Announce release on GitHub