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

dry4ts

v0.2.0

Published

Structural duplicate-code finder for TypeScript and TSX.

Downloads

510

Readme

dry4ts

dry4ts is a structural duplicate-code finder for TypeScript and TSX. It looks past names and literals to the shape of your code, so declarations that were copied once and have since drifted apart still turn up as a matching pair, reported by file and line range. It ships as a single Rust binary through npm, so npx dry4ts . runs it with no toolchain to set up.

How it works

dry4ts picks out the declarations worth comparing -- functions, methods, arrow functions, classes, interfaces, type aliases, enums, constructors, and accessors -- and rewrites each into a normalized form that keeps its structural kinds and operator/modifier markers while dropping names and values. It then fingerprints that form at every level -- the whole declaration and each node within it -- and scores each pair by how much their fingerprint sets overlap:

score = fingerprints present in both / fingerprints present in either

1.0 means the two normalized shapes are indistinguishable. The score drops as each side gains structure the other lacks. Any pair scoring at or above --threshold (default 0.82) is reported.

Structure that survives normalization includes:

  • the shape of functions, methods, and arrow functions
  • parameter lists and type annotations
  • statement blocks and their order
  • control flow: if, for, for-of, while, switch
  • assignments, returns, calls, property access, and indexing
  • array, object, and JSX literals
  • operators (+, ===, &&, ||, and the rest)
  • modifiers (export, async, public, private, static, readonly)

For instance, these two validators share no names, fields, messages, or bounds, yet they normalize to the same shape -- so dry4ts scores them 1.0:

function validateProfile(input: ProfileDraft): string[] {
  const problems: string[] = [];
  if (input.displayName.trim() === "") {
    problems.push("display name is required");
  }
  if (input.age < 13) {
    problems.push("age is below the minimum");
  }
  return problems;
}

function checkVenue(form: VenueForm): string[] {
  const issues: string[] = [];
  if (form.title.trim() === "") {
    issues.push("title is required");
  }
  if (form.capacity < 1) {
    issues.push("capacity is below the minimum");
  }
  return issues;
}

Install

Run it once, without installing anything:

npx dry4ts .

Or keep it as a dev dependency:

npm install --save-dev dry4ts

The prebuilt binary for your platform comes down automatically as an optional dependency -- there is nothing to compile and no separate download.

Usage

dry4ts [options] [file-or-directory ...]

Options:

--threshold N    report pairs scoring at least N (0.0-1.0); default 0.82
--min-lines N    skip candidates shorter than N source lines; default 4
--min-nodes N    skip candidates with fewer than N normalized nodes; default 20
--format F       "text" or "json"; default text
--json           shorthand for --format json
--text           shorthand for --format text
--ignore GLOB    exclude paths matching GLOB (repeatable; gitignore-style)
--config PATH    use PATH as the config file (skips discovery)
--no-config      ignore any .dry4tsrc.json discovery
--fail-on-found  exit 3 if any duplicate pairs are reported
--help, -h       print this help

Examples:

dry4ts .
dry4ts src/foo.ts src/bar.ts
dry4ts --json --threshold 0.9 ./src
dry4ts --ignore '**/*.test.{ts,tsx}' --fail-on-found ./src

Every path on the command line feeds into one shared comparison set. A directory argument is walked recursively for .ts, .tsx, .mts, and .cts files; .git, node_modules, and dist are always skipped, plus anything your config or --ignore excludes. CLI flags override the config file, which overrides the built-in defaults.

Configuration

Commit a .dry4tsrc.json (discovered by walking up from the working directory) so your package.json script and CI stop being flag soup:

{
  "threshold": 0.85,
  "minLines": 6,
  "minNodes": 24,
  "ignorePatterns": ["**/*.test.{ts,tsx}", "**/types/database.ts"],
  "failOnFound": true
}

Keys mirror the flags (threshold, minLines, minNodes, format, ignorePatterns, failOnFound); an unknown key is an error. ignorePatterns are gitignore-style and exclude-only: a matched file is dropped on its own (**/*.test.ts keeps its non-test siblings) and a matched directory is pruned. dry4ts does not read your .gitignore -- excludes are explicit. Use --config PATH to load an exact file or --no-config to disable discovery.

The default text format is meant to be skimmed:

DUPLICATE score=0.91
  src/forms/profile.ts:8-19
  src/forms/venue.ts:24-35

The JSON format is meant for other tools:

{
  "candidates": [
    {
      "score": 0.91,
      "left": {"file": "src/forms/profile.ts", "start_line": 8, "end_line": 19},
      "right": {"file": "src/forms/venue.ts", "start_line": 24, "end_line": 35},
      "left_nodes": 54,
      "right_nodes": 54
    }
  ]
}

With no matches, text mode prints No duplicate candidates found. and JSON mode prints { "candidates": null }.

Exit codes

  • 0 -- ran successfully (with or without matches); also --help.
  • 1 -- a file could not be read, or a directory walk failed.
  • 2 -- a bad argument or a configuration error: an unknown flag, a malformed or unreadable config, an unknown format, or an invalid ignore glob (the message and usage go to stderr).
  • 3 -- ran successfully and reported a duplicate, only under --fail-on-found (or failOnFound). Off by default.

A file that cannot be parsed is noted on stderr, skipped, and the scan continues -- still exiting 0 (or 3 under --fail-on-found). Exit 1 is reserved for genuine I/O failures.

Under the hood

Parsing is handled by oxc, a fast Rust TypeScript and JSX parser.

License

MIT. Copyright (c) 2026 sustinbebustin.