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

@lempert/user-agent

v0.4.0

Published

A Kotlin Multiplatform library that parses and generates User-Agent strings behind one common API for Android, iOS, JVM, and JS.

Readme

This is a Kotlin Multiplatform project targeting Android, iOS, JVM, and Web with a shared library module that parses and generates User-Agent strings.

Adding the dependency

Once published, the library is available on Maven Central under:

implementation("site.lempert:user-agent:0.2.0")

For JS/Node consumers, the same library is published to npm separately:

npm install @lempert/user-agent

API

The library exposes two factory functions, each composed from a variadic list of type packs:

fun UserAgentParser(vararg packs: UserAgentTypePack): (String) -> UserAgentInfo
fun UserAgentGenerator(vararg packs: UserAgentTypePack): (UserAgentInfo) -> String

Call the factory with the packs you want, then call the function it returns:

val parse = UserAgentParser(UserAgentAllTypes)
val info = parse(userAgentString) // UserAgentInfo(browser = ..., engine = ..., os = ..., device = ...)

val generate = UserAgentGenerator(UserAgentAllTypes)
val userAgentString = generate(info)

Built-in packs, each individually importable so a bundler can tree-shake out the ones you don't reference:

  • UserAgentBrowserTypes -- populates UserAgentInfo.browser
  • UserAgentEngineTypes -- populates UserAgentInfo.engine
  • UserAgentOsTypes -- populates UserAgentInfo.os
  • UserAgentDeviceTypes -- populates UserAgentInfo.device
  • UserAgentBotTypes -- populates UserAgentInfo.bot: 26 entries -- a mix of search/SEO crawlers, monitoring tools, social/link-preview bots, API clients, and scraping/extraction services -- see UserAgentBotTypePack.kt for the full entry list and sourcing notes
  • UserAgentAIAgentTypes -- populates UserAgentInfo.aiAgent: 20 well-known AI/LLM crawlers and agents -- see UserAgentAIAgentTypePack.kt for the full entry list and sourcing notes
  • UserAgentAllTypes -- convenience bundle of all of the above

Every UserAgentBotTypes/UserAgentAIAgentTypes entry is hand-transcribed directly from that bot/crawler operator's own public documentation, or -- only where no first-party page could be found -- from multiple independent, clearly-attributed corroborating sources with a known operator (see the two files linked above's KDoc comments for the operators sourced from) -- never copied from a third-party commercial bot-detection dataset. Both tables are intentionally non-exhaustive starter lists; add your own entries via a custom UserAgentTypePack (see below) if you need to detect something not covered here.

Passing no packs returns an always-empty result -- every UserAgentInfo field null on parse, or just the bare "Mozilla/5.0" base string on generate. There is no implicit fallback to UserAgentAllTypes; a consumer wanting full detection passes it explicitly. This keeps a single-pack import (e.g. only UserAgentBrowserTypes) tree-shakeable in a JS build -- the unused packs' rule tables and detection code are never bundled.

You can pass a subset of packs to only populate the fields you care about:

val parseBrowserOnly = UserAgentParser(UserAgentBrowserTypes)
val info = parseBrowserOnly(userAgentString) // only `browser` is populated; the rest are null

When more than one pack is passed, UserAgentParser merges their detect results field-by-field: the first pack (in the order given) to produce a non-null value for a field wins, and UserAgentInfo.custom entries merge by key with the same first-pack-wins rule per key. UserAgentGenerator instead tries each pack's applyToGenerate in order and uses the first non-null result, falling back to the bare "Mozilla/5.0" base string if every pack returns null.

JS/TypeScript usage

The same API is published to npm. Top-level pack constants are exported as getter objects under Kotlin/JS's @JsExport lowering, so call .get() to retrieve the actual UserAgentTypePack instance, and pass packs as a plain array rather than varargs:

import { UserAgentBrowserTypes, UserAgentParser } from '@lempert/user-agent';

const parse = UserAgentParser([UserAgentBrowserTypes.get()]);
const info = parse(userAgentString); // only `browser` is populated; the rest are null

You can also author your own pack -- a UserAgentTypePack is just an id, a detect function, and an optional applyToGenerate function -- to add detection or generation categories without forking the library:

val myPack = UserAgentTypePack(
    id = "myThing",
    detect = { userAgent -> UserAgentInfo(custom = mapOf("myThing" to Component("Found", null))) },
    applyToGenerate = { info -> info.custom["myThing"]?.let { "MyThing/${it.name}" } },
)
val parse = UserAgentParser(UserAgentBrowserTypes, myPack)
val generate = UserAgentGenerator(UserAgentBrowserTypes, myPack)

A pack that throws during detect/applyToGenerate degrades gracefully -- it just contributes nothing for that call, and never crashes a composed UserAgentParser/UserAgentGenerator call.

UserAgentInfo's bot/aiAgent fields are populated by UserAgentBotTypes/ UserAgentAIAgentTypes (or UserAgentAllTypes) above; they stay null when those packs aren't passed, or when nothing in the passed packs' tables matches.

Migrating from 0.1.0

0.1.0's singleton API is gone in 0.2.0 -- replace it with the pack-based factory functions above, passing UserAgentAllTypes to match the old, all-categories behavior:

// 0.1.0
val info = UserAgentParser.parse(userAgentString)
val userAgentString = UserAgentGenerator.generate(info)

// 0.2.0
val info = UserAgentParser(UserAgentAllTypes)(userAgentString)
val userAgentString = UserAgentGenerator(UserAgentAllTypes)(info)

UserAgentInfo also gained two new trailing fields in 0.2.0, bot: Component? and aiAgent: Component? (both default to null, unused until a future release's bot/AI-agent packs). This matters if you construct UserAgentInfo positionally (as webApp's sample does from TypeScript) or serialize/deserialize it, since the field count/order changed.

Project structure

  • /library is the multiplatform library itself -- UserAgentParser, UserAgentGenerator, the built-in type packs, and the shared data model live in commonMain, with the shared cross-target test corpus in commonTest. Every production source set depends only on the Kotlin stdlib.

  • /androidApp, /iosApp, /jvmApp, and /webApp are thin per-target sample apps -- one per MVP target (Android, iOS, JVM, Web) -- that each depend on :library and call UserAgentParser(UserAgentAllTypes)/UserAgentGenerator(UserAgentAllTypes) to prove the library works as a consumed dependency. They are harnesses, not real app experiences.

Running the apps

Use the run configurations provided by the run widget in your IDE's toolbar. You can also use these commands and options:

  • Android app: ./gradlew :androidApp:assembleDebug
  • JVM app: ./gradlew :jvmApp:run
  • Web app:
    1. Install Node.js (which includes npm)
    2. Build and run the web application:
      npm run build:shared
      npm install
      npm run start
  • iOS app: open the /iosApp directory in Xcode and run it from there.

Running tests

Use the run button in your IDE's editor gutter, or run tests using Gradle tasks:

  • All targets at once: ./gradlew :library:allTests
  • Android tests: ./gradlew :library:testAndroidHostTest
  • JVM tests: ./gradlew :library:jvmTest
  • Web tests: ./gradlew :library:jsTest
  • iOS tests: ./gradlew :library:iosSimulatorArm64Test

CI (.github/workflows/ci.yml) runs ./gradlew build on every push and pull request, which exercises all four targets and compiles the sample apps.


Learn more about Kotlin Multiplatform