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

@ryanke/spider

v0.1.3

Published

A simple toolkit to scrape websites quickly and easily with full type safety. Inspired by [x-ray](https://github.com/matthewmueller/x-ray#readme).

Readme

@ryanke/spider

A simple toolkit to scrape websites quickly and easily with full type safety. Inspired by x-ray.

example

// lets scrape https://news.ycombinator.com/!
// schemas takes HTML and spits out a structured object with type information built in.
const schema = s({
  posts: s([
    // include the "tr" immediately after the ".athing" element,
    // then apply the following schema to each and format the results into an array.
    "tr.athing ~ tr.athing+tr",
    s({
      title: s(".titleline > a"),
      // parse as number
      // most parsers are fairly lenient and will try to parse just about anything
      // the number parser can take values such as "one million", "1.5k", "1000", "10,000 dollars", etc
      posiition: s("td span.rank").number(),
      // the "optional" filter will return undefined if the selector doesn't match anything
      // without this, a MissingValue error will be thrown if the selector matches nothing
      points: s("td span.score").number().optional(),
      comments: s("a:contains(comment)").number().optional(),
      // "@" can be used to get attributes, like "href", "innerHTML", etc
      url: s(".titleline a@href"),
      // parse a date. this could be something like "3 hours ago", "2021-01-01", "yesterday", etc.
      createdAt: s("span.age@title").date(),
      // a comma-separated list of selectors, the first one that matches will be used
      commentsUrl: s("a:contains(comment)@href, a:contains(discuss)@href").optional(),
      author: s({
        name: s("a.hnuser").trim().optional(), // trim whitespace
        url: s("a.hnuser@href").optional(),
      }),
    }),
  ]),
});

const html = await fetch("https://news.ycombinator.com/").then((res) => res.text());
const data = schema.parseHTML(html);

// the return type will be inferred based on the schema, including the types of the fields when using transformers.
data.posts[0].comments; // "number" type

// if you want, s() could also be used outside an object.
const title = s("h1.title").parseHTML(html);

transformers

Transformers are used to convert plain text into more useful types. They have full type safety, the return type is inferred based on the transformer used. Generally, built in transformers will try to be lenient and correct to avoid surprises.

// this will parse things like "1200" and "1.2k" into 1200
const number = s("span").number(); 

// this parses dates from formats like "3pm", "2021-01-01", "yesterday", "3 years ago", etc to a date.
// for relative dates, it uses the current time as a reference point. this can make it inaccurate for
// a time like "a decade ago" but... ¯\_(ツ)_/¯
const date = s("span").date();

// converts "yes", "no", "true", "false", etc to true or false.
const bool = s("span").boolean();

// converts HTML elements to markdown, which might be a more convenient format.
// "@innerHTML" is important or else it will run on the text content and not the HTML itself.
const markdown = s("div@innerHTML").markdown();

// converts "Key: value", "key=value" or "2.5/10" into just the "value" and "2.5" parts respectively
const value = s("li").value();

// converts a value to an enum with lax matching.
enum MyEnum {
  // "1", "one", "One", "one with a long name", "One With A Long Name", "OneWithALongName" will all match this value
  OneWithALongName = 1,
  // "2", "two", "Two" will match this value
  Two = 2,
}

const enumValue = s("span").enum(MyEnum);

// splits a string, stripping quotes around the values.
// - "'one', 'two', 'three'" becomes ["one", "two", "three"].
// "one | two | three" becomes ["one", "two", "three"].
// "a OR b" becomes ["a", "b"].
const split = s("span").split();

// casing utils
const camelCase = s("span").camelcase(); // "hello world" -> "helloWorld"
const upperCase = s("span").uppercase(); // "hello world" -> "HELLO WORLD"
const lowerCase = s("span").lowercase(); // "Hello World" -> "hello world"
const titleCase = s("span").titlecase(); // "hello world" -> "Hello World"

// replacing values in the target
const replace = s("span").replace("world", "universe"); // "hello world" -> "hello universe"

// there are more built-in transformers, look in src/transforms for a full list.
// you can also register your own. types are inferred based on the return type of the transformer.
const custom = s("span").transform((input: string) => Number(input)); // parseHTML() return type is "number"

// this may be incomplete. see src/transformers and src/builder.ts for a full list of built-in transformers.
// .. or just use intellisense.

custom selectors

Sometimes sites are stubborn and have data in a format you can't easily parse using just selectors and transformers. For this, in place of selectors you can use a function which is passed the cheerio object and, for arrays, the element that is being processed. This function can return any value, and can do whatever necessary to grab data

If a custom extraction function returns an array, all results from each iteration will be flattened into a single array.

const mySchema = s({
  title: s(($) => {
    return $("title").text();
  }),
  tags: [
    "#tag-list div",
    s(($, el) => {
      // "el" is available because this is an array, so "el" corresponds
      // to the current element and this function will be called on each.
      return $(el).text();
    }),
  ],
});

todo

  • Zod-based validation
    • We have a lot of utilities that mimic zod types (eg, .min()), it would make more sense to integrate with zod directly.
    • For example, transform a value directly into an enum with .nativeEnum(Enum)
    • Not sure how to do this because we would have to proxy the schema object to zod? Maybe just that, proxy the schema object and maintain an internal schema that is checked.