npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

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

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 difftar
pnpm add difftar
yarn add difftar
bun add difftar

Your 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.tgz

CLI 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_xxxxx

CLI 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 string

extractPackage(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