Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/app/components/BadgeGeneratorParameters.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const badgeColor = useState('badge-color', () => '')
const usePkgName = useState('badge-use-name', () => false)
const badgeStyle = useState('badge-style', () => 'default')

const styles = ['default', 'shieldsio']
const styles = ['default', 'shieldsio', 'compact']

const validateHex = (hex: string) => {
if (!hex) return true
Expand Down
74 changes: 72 additions & 2 deletions docs/content/2.guide/6.badges.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,72 @@ npmx.dev offers many different SVG badges with stats about any package via its A
[![Open on npmx.dev](https://npmx.dev/api/registry/badge/version/react/v/18.0.0)](https://npmx.dev/package/react)
```

## Compare Badges

Compare badges show how a stat differs between two pinned package versions using a `from → to` value. They support both **same-package** comparisons (e.g. how `nuxt` changed between `2.18.1` and `4.3.1`) and **cross-package** comparisons (e.g. `nuxt` vs. `next`). They share the same look, fonts and styles as the regular badges and accept the same customization parameters.

### URL pattern

**Same-package** (shorthand version range):

```
/api/registry/badge/compare/{type}/{package}/v/{from}...{to}
```

The version range uses the same triple-dot (`...`) syntax as the rest of the npmx.dev compare API.

**Cross-package** (`vs` separator between two pinned `pkg@version` specs):

```
/api/registry/badge/compare/{type}/{pkgA}/v/{verA}/vs/{pkgB}/v/{verB}
```

In all forms, both versions must already exist on npm — unknown versions return `404`.

The badge stays visually compact: only the raw `from → to` values are rendered, so a cross-package size compare reads `52.7 KB → 200 KB` rather than carrying the package names inline. The package context lives in the URL and the SVG `aria-label`. With `name=true` the label switches from the strategy name to `{pkgA} → {pkgB}`, mirroring the regular single-package `name=true` behavior.

Because the data for two pinned versions is immutable, compare badges are cached for one year (vs. one hour for the regular badges).

### Available Compare Badge Types

- **version**: `v{from} → v{to}`. Always blue.
- **size**: install size delta (Bundlephobia, falls back to packument `dist.unpackedSize`). Color is **green** when the size shrunk by ≥5%, **red** when it grew by ≥5%, **slate** otherwise.
- **dependencies**: total runtime dependency count delta. Color follows the same direction logic as `size` (more deps = red, fewer = green).
- **license**: `{from} → {to}` license. **Green** when the license is unchanged across versions, **yellow** when it changed.
- **engines**: supported `engines.node` range. **Slate** when the supported range is unchanged, **yellow** when it changed.

Compare-incompatible badge types (`name`, `created`, `updated`, `downloads*`, `maintainers`, `likes`, `types`, `vulnerabilities`, `deprecated`) are not exposed under `/badge/compare/...` and return `404`.

### Examples

```md
# Same-package version delta

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/version/nuxt/v/3.0.0...3.21.0)](https://npmx.dev/package/nuxt)

# Same-package install size delta (directional color)

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/size/nuxt/v/3.0.0...3.21.0)](https://npmx.dev/package/nuxt)

# Dependency count delta on a scoped package

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/dependencies/@nuxt/kit/v/3.20.0...3.21.0)](https://npmx.dev/package/@nuxt/kit)

# Cross-package install size comparison

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/size/nuxt/v/4.3.1/vs/next/v/15.5.11)](https://npmx.dev/package/nuxt)

# Cross-package dependencies comparison

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/dependencies/nuxt/v/4.3.1/vs/next/v/15.5.11)](https://npmx.dev/package/nuxt)

# Compact compare badge (e.g. for tight READMEs)

[![Open on npmx.dev](https://npmx.dev/api/registry/badge/compare/dependencies/nuxt/v/3.0.0...3.21.0?style=compact)](https://npmx.dev/package/nuxt)
```

All [Customization Parameters](#customization-parameters) below — `label`, `value`, `color`, `labelColor`, `name`, `style` — work identically on compare badges. A user-supplied `color` overrides the directional color.

## Customization Parameters

You can further customize your badges by appending query parameters to the badge URL.
Expand Down Expand Up @@ -114,7 +180,11 @@ When set to `true`, this parameter replaces the static category label (like "ver

### `style`

Overrides the default badge appearance. Pass `shieldsio` to use the shields.io-compatible style.
Overrides the badge appearance.

- `default` — the standard npmx.dev look at 20px tall.
- `shieldsio` — the classic shields.io-compatible look at 20px tall, useful when you need the badge to sit alongside existing shields.io badges.
- `compact` — the same modern look and 20px height as `default` but with tight 5px text padding and no enforced minimum side width. Long built-in labels are also shortened (e.g. `install size` → `size`, `downloads/mo` → `dl/mo`, `dependencies` → `deps`, `maintainers` → `maint`) so the badge can take up roughly 20–50% less horizontal space in READMEs. Pass an explicit `label` or `name=true` to opt out of the shortening.

- **Default**: `default`
- **Usage**: `?style=shieldsio`
- **Usage**: `?style=compact` or `?style=shieldsio`
16 changes: 13 additions & 3 deletions modules/runtime/server/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,14 +122,24 @@ function getMockForUrl(url: string): MockResult | null {
return { data: null }
}

// Bundlephobia API - return mock size data
// Bundlephobia API - return mock size data.
// When a version is supplied, the size is derived deterministically from the
// version string so that compare-badge tests can verify per-version size
// deltas. Without a version we fall back to the legacy fixture value.
if (host === 'bundlephobia.com' && pathname === '/api/size') {
const packageSpec = searchParams.get('package')
if (packageSpec) {
const parsed = parseScopedPackageWithVersion(packageSpec)
let size = 12345
if (parsed.version) {
let h = 0
for (const ch of parsed.version) h = (h * 31 + ch.charCodeAt(0)) >>> 0
size = 10000 + (h % 50000)
}
return {
data: {
name: packageSpec.split('@')[0],
size: 12345,
name: parsed.name,
size,
gzip: 4567,
dependencyCount: 3,
},
Expand Down
Loading
Loading