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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@interlynk-io/purl-js

v1.0.2

Published

A spec-compliant Package URL (PURL) parser, builder, and validator with full type registry

Readme

@interlynk-io/purl-js

A spec-compliant Package URL (PURL) parser, builder, and validator for JavaScript/TypeScript.

Features

  • ECMA-427 compliant — passes all 514 official purl-spec tests
  • All 38 registered types — full type-specific normalization and validation
  • Zero runtime dependencies — only typescript and vitest as dev dependencies
  • Immutable & type-safe — frozen instances, null-prototype qualifiers, full TypeScript types
  • Secure by default — null byte rejection, input length limits, prototype pollution protection
  • Fast — 34M ops/sec for typical package name encoding (see Performance)
  • 752 tests — 514 spec + 210 ECMA-427 compliance + 28 security

Installation

npm install @interlynk-io/purl-js

Quick Start

import { PackageURL, tryParse, isValid, validateString } from '@interlynk-io/purl-js';

// Parse a PURL string
const purl = PackageURL.parse('pkg:npm/%40angular/[email protected]');
console.log(purl.type);      // "npm"
console.log(purl.namespace);  // "@angular"
console.log(purl.name);       // "core"
console.log(purl.version);    // "16.2.0"

// Build a PURL from components
const built = new PackageURL('pypi', null, 'Django', '4.2', null, null);
console.log(built.toString()); // "pkg:pypi/[email protected]"

// Safe parsing (returns null instead of throwing)
const result = tryParse('not-a-purl'); // null

// Validation
isValid('pkg:npm/[email protected]');  // true
isValid('pkg:julia/Flux');           // false (requires uuid qualifier)

Use Cases

Parse and inspect SBOMs

const components = sbom.components.map(c => {
  const purl = tryParse(c.purl);
  return {
    ecosystem: purl?.type,
    package: purl?.name,
    version: purl?.version,
    scope: purl?.namespace,
  };
});

Compare package versions

const current = PackageURL.parse('pkg:npm/[email protected]');
const updated = PackageURL.parse('pkg:npm/[email protected]');

if (current.matchesBase(updated) && !current.equals(updated)) {
  console.log(`Update available: ${current.version} -> ${updated.version}`);
}

Validate user input

function handlePurlInput(input: string) {
  const err = validateString(input);
  if (err) {
    return { valid: false, errors: err.errors.map(e => e.message) };
  }
  return { valid: true, purl: PackageURL.parse(input) };
}

Build PURLs programmatically

new PackageURL('npm', '@angular', 'core', '16.2.0', null, null).toString();
// "pkg:npm/%40angular/[email protected]"

new PackageURL('maven', 'org.junit', 'junit', '5.0', { classifier: 'sources' }, null).toString();
// "pkg:maven/org.junit/[email protected]?classifier=sources"

new PackageURL('docker', 'library', 'nginx', 'latest', { arch: 'amd64' }, null).toString();
// "pkg:docker/library/nginx@latest?arch=amd64"

Type-specific normalization

The library automatically applies normalization rules per type:

// PyPI: underscores become dashes, name lowercased
PackageURL.parse('pkg:pypi/[email protected]').toString();
// "pkg:pypi/[email protected]"

// npm: namespace and name lowercased
PackageURL.parse('pkg:npm/%40Angular/[email protected]').toString();
// "pkg:npm/%40angular/[email protected]"

// Hugging Face: version (commit hash) lowercased
PackageURL.parse('pkg:huggingface/google/bert-base-uncased@CD5EF3A3').toString();
// "pkg:huggingface/google/bert-base-uncased@cd5ef3a3"

API Reference

PackageURL Class

All instances are immutable. Properties are readonly.

| Property | Type | Description | | ------------ | -------------------------------- | -------------------------- | | type | string | Package type (e.g., npm) | | namespace | string \| null | Package namespace/scope | | name | string | Package name | | version | string \| null | Package version | | qualifiers | Record<string, string> \| null | Key-value qualifier pairs | | subpath | string \| null | Subpath within the package |

Parsing & Building

| Method | Description | | ------ | ----------- | | PackageURL.parse(purl: string): PackageURL | Parse a PURL string (right-to-left algorithm). Throws on invalid input. | | new PackageURL(type, namespace, name, version, qualifiers, subpath) | Build from components with type-specific normalization and validation. | | toString(): string | Return the canonical PURL string. |

Transformations

| Method | Description | | ------ | ----------- | | withVersion(version: string): PackageURL | New instance with a different version. | | withoutVersion(): PackageURL | New instance with version removed. | | withQualifiers(qualifiers: Record<string, string>): PackageURL | New instance with replaced qualifiers. |

Comparison & Validation

| Method | Description | | ------ | ----------- | | equals(other: PackageURL): boolean | Semantic equality after normalization. | | matchesBase(other: PackageURL): boolean | Same package ignoring version, qualifiers, subpath. | | getQualifier(key: string): string \| undefined | Get a single qualifier value. | | validate(): ValidationError \| null | Full spec validation including type-specific rules. |

Utility Functions

| Function | Description | | -------- | ----------- | | tryParse(purl: string): PackageURL \| null | Parse without throwing — returns null on failure. | | isValid(purl: string): boolean | Check if a string is a valid PURL. | | validateString(purl: string): ValidationError \| null | Parse + validate in one step. Returns error details or null. |

Type Registry

| Function | Description | | -------- | ----------- | | lookupType(type: string): TypeDefinition \| undefined | Look up a registered type definition (returns frozen object). | | registeredTypes(): string[] | All registered type identifiers, sorted alphabetically. | | registerType(def: TypeDefinition): void | Register a custom type. Cannot override the 38 built-in spec types. |

import { lookupType, registeredTypes, registerType } from '@interlynk-io/purl-js';

const pypi = lookupType('pypi');
console.log(pypi?.namespace.requirement);  // "prohibited"
console.log(pypi?.name.caseSensitive);     // false

console.log(registeredTypes());
// ["alpm", "apk", "bazel", "bitbucket", "bitnami", "cargo", ...]

registerType({
  type: 'custom',
  typeName: 'Custom',
  description: 'My custom package type',
  repository: { useRepository: false },
  namespace: { requirement: 'optional', caseSensitive: true },
  name: { requirement: 'required', caseSensitive: true },
  version: { requirement: 'optional', caseSensitive: true },
  examples: ['pkg:custom/[email protected]'],
});

Supported Types

| Type | Description | Namespace | | ---- | ----------- | --------- | | alpm | Arch Linux packages | required | | apk | APK-based packages | required | | bazel | Bazel modules | prohibited | | bitbucket | Bitbucket repos | required | | bitnami | Bitnami packages | prohibited | | cargo | Rust Cargo crates | prohibited | | cocoapods | CocoaPods | optional | | composer | PHP Composer | required | | conan | C/C++ Conan | optional | | conda | Conda packages | optional | | cpan | Perl CPAN | required | | cran | R packages | prohibited | | deb | Debian packages | required | | docker | Docker images | optional | | gem | Ruby gems | optional | | generic | Generic packages | optional | | github | GitHub repos | required | | golang | Go packages | required | | hackage | Haskell packages | optional | | hex | Hex packages | optional | | huggingface | Hugging Face models | required | | julia | Julia packages | prohibited | | luarocks | Lua packages | optional | | maven | Maven artifacts | required | | mlflow | MLflow models | prohibited | | npm | Node npm packages | optional | | nuget | .NET NuGet | prohibited | | oci | OCI images | prohibited | | opam | OCaml packages | optional | | otp | Erlang/OTP | prohibited | | pub | Dart/Flutter pub | optional | | pypi | Python packages | prohibited | | qpkg | QNX packages | optional | | rpm | RPM packages | required | | swid | ISO SWID tags | optional | | swift | Swift packages | required | | vscode-extension | VS Code extensions | required | | yocto | Yocto recipes | required |

Spec Compliance

Fully compliant with ECMA-427 / TC54. The test suite includes all 514 official spec tests sourced from the upstream package-url/purl-spec repository, plus a dedicated 210-test ECMA-427 compliance suite.

Verified:

  • Right-to-left parsing algorithm
  • Percent-encoding (unreserved set A-Za-z0-9.-_~:, uppercase hex)
  • Checksum qualifier %2C encoding
  • Subpath ./.. discard and %2F rejection
  • Qualifier key validation and duplicate rejection
  • Namespace %2F segment boundary protection
  • Scheme validation
  • Type-specific normalization (PyPI _-, npm lowercase, Hugging Face version lowercase, MLflow conditional case)
  • Namespace required/prohibited enforcement
  • Required qualifier enforcement (Julia uuid, SWID tag_id)
  • CPAN :: rejection
  • npm @scope handling

Security

Designed for untrusted input. Key protections:

  • Input limitsparse() enforces 64 KB max input, 128-qualifier limit, 4 KB per-component limit
  • Injection prevention — null bytes (\0, %00) rejected in all fields and during percent-decoding
  • Immutability — qualifier objects use Object.create(null) (no prototype pollution) and Object.freeze
  • Internal bypass protection — constructor skip-normalization gated by module-private Symbol
  • Registry protection — built-in types cannot be overridden via registerType()
  • Defense-in-depthtoString() re-filters .. from subpath; error messages truncate attacker-controlled strings; validate() catches invalid states from manual object construction
// For untrusted input, prefer parse() or tryParse()
const purl = tryParse(userInput);
if (!purl) { /* handle invalid input */ }

// If using the constructor with user-supplied components, validate afterward
const built = new PackageURL(type, ns, name, ver, quals, sub);
const err = built.validate();
if (err) { /* handle validation errors */ }

Performance

Optimized for the typical PURL workload: short ASCII strings with occasional percent-encoding.

Run with npx vitest bench. Results on Apple M-series (Node.js 24):

| Function | Input | ops/sec | Latency | |----------|-------|---------|---------| | percentEncode | Short ASCII (7 chars) | 34M | 29 ns | | | Mixed (46 chars) | 2.7M | 370 ns | | | Unicode (12 chars) | 2.9M | 340 ns | | | Long (1550 chars) | 150K | 6.6 μs | | percentDecode | Short ASCII | 38M | 26 ns | | | Mixed | 4.9M | 206 ns | | | Unicode | 4M | 252 ns | | | Long | 583K | 1.7 μs |

Optimizations:

  • Fast-path short-circuit for all-ASCII strings
  • Pre-computed 256-entry byte→encoded lookup table
  • Shared TextEncoder instance
  • Fast-path decode when no % present
  • O(1) slash stripping via index scanning

Development

Setup

npm install

Tests

npm test              # Run all 752 tests
npm run test:watch    # Watch mode
npm run test:coverage # With coverage

Benchmarks

npx vitest bench

Build

npm run build  # Outputs to ./dist

Playground

Interactive browser-based playground for exploring PURL parsing, building, and validation. No build step required — Vite serves TypeScript directly.

npx vite --open playground.html --port 5555

Four tabs:

  • Parse — enter a PURL string, see parsed components and canonical form. Quick-try buttons for npm, maven, pypi, docker, golang, and oci.
  • Build — select a type, fill in components, add qualifiers dynamically. Namespace auto-disables for types that prohibit it.
  • Validate — green/red validation with detailed error cards (field, code, message). Quick-try buttons for common failures.
  • Type Registry — browse all 38 types with namespace/name/version rules, case sensitivity, qualifiers, and clickable examples.

License

Apache-2.0