difftar
v1.0.0
Published
RAWR! I'm Difftar! The giant green tarball-diffing monster that stomps through your npm packages! Fear my unified diffs!
Downloads
82
Maintainers
Readme
Why Difftar?
You know npm diff? It's great for comparing package versions locally. But what if you need to:
- Run diffs on the edge - in a Cloudflare Worker, Deno Deploy, or serverless function?
- Compare tarballs from different sources - S3, R2, private registries, or inline data?
- Integrate diff into your API - without shelling out to the npm CLI?
Difftar brings the power of npm diff to any JavaScript runtime. Same output format, zero native dependencies, runs anywhere.
Quick Start
Install
npm install difftarpnpm add difftar
yarn add difftar
bun add difftarYour First Diff
import { diff } from 'difftar';
const patch = await diff(
{ transport: 'url', source: 'https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz' },
{ transport: 'url', source: 'https://registry.npmjs.org/is-odd/-/is-odd-3.0.1.tgz' }
);
console.log(patch);Or Use the CLI
# Using package specs (like npm diff)
npx difftar --diff [email protected] --diff [email protected]
# Using URLs directly
npx difftar https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz \
https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgzCLI Usage
difftar <left-url> <right-url> [options]
difftar --diff <spec> --diff <spec> [options]Common Examples
# Compare two versions of a package
difftar --diff [email protected] --diff [email protected]
# Show only changed file names
difftar --diff [email protected] --diff [email protected] --diff-name-only
# Ignore whitespace changes
difftar --diff [email protected] --diff [email protected] --diff-ignore-all-space
# Private registry with authentication
difftar --diff @myorg/[email protected] --diff @myorg/[email protected] \
--auth=bearer --token=npm_xxxxxCLI Options
| Option | Description |
|--------|-------------|
| --diff <spec> | Package spec (e.g., [email protected]) or tarball URL. Use twice. |
| --diff-name-only | Only show changed file names |
| --diff-unified=N | Number of context lines (default: 3) |
| --diff-ignore-all-space | Ignore all whitespace changes |
| --diff-ignore-space-change | Ignore changes in whitespace amount |
| --diff-no-prefix | Remove a/ b/ prefixes from paths |
| --diff-src-prefix=X | Source prefix (default: a/) |
| --diff-dst-prefix=X | Destination prefix (default: b/) |
| --diff-text | Treat all files as text (including binary) |
| --auth=bearer\|basic | Authentication type |
| --token=TOKEN | Auth token or base64 credentials |
API
diff(left, right, options?)
Compare two tarballs and return a unified diff string.
import { diff } from 'difftar';
const patch = await diff(
{ transport: 'url', source: 'https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz' },
{ transport: 'url', source: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' },
{ context: 5 }
);Source Configuration
Each source (left/right) is configured with a SourceConfig object:
type SourceConfig = {
transport: 'url' | 's3' | 'inline' | 'file';
source?: string; // URL, S3 URI, or file path
auth?: 'none' | 'basic' | 'bearer';
credential?: string; // Token or base64(user:pass)
s3?: S3Config; // For S3 transport
data?: Uint8Array | string; // For inline transport
};Transport Examples
URL (public registry)
{ transport: 'url', source: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz' }URL (private registry with bearer token)
{
transport: 'url',
source: 'https://npm.pkg.github.com/@org/pkg/-/pkg-1.0.0.tgz',
auth: 'bearer',
credential: process.env.GITHUB_TOKEN
}URL (basic auth)
{
transport: 'url',
source: 'https://registry.example.com/pkg/-/pkg-1.0.0.tgz',
auth: 'basic',
credential: btoa('username:password')
}S3 (AWS)
{
transport: 's3',
source: 's3://my-bucket/packages/pkg-1.0.0.tgz',
s3: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
region: 'us-east-1'
}
}S3 (Cloudflare R2)
{
transport: 's3',
source: 's3://my-bucket/pkg.tgz',
s3: {
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
region: 'auto',
endpoint: 'https://account-id.r2.cloudflarestorage.com'
}
}Inline (Uint8Array or base64)
// For npm publish _attachments or testing
{ transport: 'inline', data: tarballUint8Array }
{ transport: 'inline', data: base64EncodedString }File (Node.js/Bun/Deno only)
{ transport: 'file', source: '/path/to/package.tgz' }Diff Options
type DiffOptions = {
nameOnly?: boolean; // Only output file names
ignoreAllSpace?: boolean; // Ignore all whitespace
ignoreSpaceChange?: boolean; // Ignore whitespace amount changes
context?: number; // Context lines (default: 3)
noPrefix?: boolean; // Remove a/ b/ prefixes
srcPrefix?: string; // Source prefix (default: 'a/')
dstPrefix?: string; // Destination prefix (default: 'b/')
text?: boolean; // Treat binary files as text
};diffWithStats(left, right, options?)
Like diff(), but returns metadata about the changes:
import { diffWithStats } from 'difftar';
const result = await diffWithStats(left, right);
console.log(`${result.filesChanged} files changed`);
console.log(`${result.filesAdded} files added`);
console.log(`${result.filesDeleted} files deleted`);
console.log(result.output); // The diff stringextractPackage(config)
Extract a tarball without computing a diff. Useful for inspection:
import { extractPackage } from 'difftar';
const files = await extractPackage({
transport: 'url',
source: 'https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz'
});
for (const [path, content] of files) {
console.log(`${path}: ${content.length} bytes`);
}Error Handling
Difftar uses typed errors with phases for proper HTTP status mapping:
import { diff, DiffError, isDiffError } from 'difftar';
try {
await diff(left, right);
} catch (error) {
if (isDiffError(error)) {
console.log(`Phase: ${error.phase}`); // 'FETCH' | 'DECOMPRESS' | 'TAR' | 'DIFF' | 'AUTH' | 'SIZE'
console.log(`Status: ${error.status}`); // HTTP status code
console.log(`Message: ${error.message}`);
// For API responses:
return error.toResponse(); // Returns a Response object
}
}| Phase | HTTP Status | When |
|-------|-------------|------|
| AUTH | 401 | Invalid or expired credentials |
| SIZE | 413 | Package exceeds 20MB limit |
| FETCH | 502 | Network failure |
| DECOMPRESS | 422 | Invalid gzip data |
| TAR | 422 | Invalid tar structure |
| DIFF | 500 | Internal error |
Edge Deployment
Difftar is designed for edge runtimes. Here's a Cloudflare Worker example:
import { diff, DiffError } from 'difftar';
export default {
async fetch(request) {
const { left, right, options } = await request.json();
try {
const patch = await diff(left, right, options);
return new Response(patch, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
});
} catch (error) {
if (error instanceof DiffError) {
return error.toResponse();
}
return new Response('Internal error', { status: 500 });
}
}
};Platform Compatibility
Difftar runs on WinterTC-compatible JavaScript runtimes:
| Platform | Supported | Memory | CPU Time | Wall Time | Notes | |----------|-----------|--------|----------|-----------|-------| | Node.js 18+ | Yes | ~4GB | None | None | Full features | | Bun | Yes | ~4GB | None | None | Full features | | Deno | Yes | 512MB | 50s | 50s | Full features | | Google Cloud Run | Yes | Configurable | Configurable | 60m | Full features | | Cloudflare Workers | Yes | 128MB | 30s | 30s | No filesystem | | Fastly Compute | Yes | 128MB | 50ms+ | 120s | No filesystem |
Transport Compatibility
| Transport | Node.js | Bun | Deno | Cloud Run | CF Workers | Fastly Compute |
|-----------|---------|-----|------|-----------|------------|----------------|
| url | Yes | Yes | Yes | Yes | Yes | Yes |
| s3 | Yes | Yes | Yes | Yes | Yes | Yes |
| inline | Yes | Yes | Yes | Yes | Yes | Yes |
| file | Yes | Yes | Yes | Yes | No | No |
Architecture
Difftar processes tarballs through a monster-themed pipeline:
CHOMP CRUNCH TEAR STOMP ROAR
(Fetch) -> (Decompress) -> (Untar) -> (Diff) -> (Format)| Phase | What it does |
|-------|-------------|
| CHOMP | Fetches tarball bytes from URL, S3, file, or inline data |
| CRUNCH | Decompresses gzip using native DecompressionStream |
| TEAR | Extracts tar entries to in-memory file map |
| STOMP | Computes Myers diff between file trees |
| ROAR | Formats output as unified diff (same as npm diff) |
Limitations
Size Limit: 20MB per tarball
Difftar enforces a 20MB limit per tarball. This covers approximately 98% of packages on npm (based on npm-high-impact analysis).
Why? Edge platforms have memory constraints (128MB on Cloudflare Workers). Two 25MB compressed tarballs could expand to 200MB+ uncompressed, causing OOM errors.
If you need to diff larger packages, run Difftar on Node.js, Bun, or a container-based platform like Google Cloud Run.
File Transport
The file transport requires filesystem access and won't work on Cloudflare Workers or other sandboxed edge runtimes. Use url, s3, or inline transports on those platforms.
License
MIT
