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

@hypercrab/roll-in

v0.3.1

Published

TypeScript dependency inlining, as a reading aid.

Downloads

554

Readme

roll-in

TypeScript dependency inlining, as a reading aid.

A function and everything it transitively calls, rendered as one self-contained piece of code — so behavior can be read in one place instead of chased across files.

npx @hypercrab/roll-in src/orders.ts processOrder

It reads your project's own tsconfig.jsonpaths, baseUrl, module resolution and all — so there is nothing to configure and nothing to prepare.


Example

// src/main.ts
import { classify, type Score } from "./classify.js";

export function describeScore(score: Score): string {
  return `${score.label} is ${classify(score)}`;
}
roll-in src/main.ts describeScore
/*
 * roll-in — `describeScore`
 *
 *   Source     src/main.ts:3-5
 *   Signature  describeScore(score: Score): string
 *   Depth      2 of 3 allowed
 *
 * Inlined from this project
 *   classify ← classify  src/classify.ts:8-14   1 call site
 *
 * Types inlined from this project
 *   Band ← Band    src/classify.ts:1   type, 1 reference, with `classify`
 *   Score ← Score  src/classify.ts:3-6   type, 2 references, above `describeScore`
 *
 * External — type only, source never read
 *   Math   lib.es5.d.ts   Math
 *   floor  lib.es5.d.ts   (x: number) => number
 *
 * This artifact is for reading. Identifiers from other modules are left as
 * written, so it is not guaranteed to compile or run.
 */

// Score ← src/classify.ts:3-6  (`Score`)
type Score = {
  readonly label: string;
  readonly value: number;
};

function describeScore(score: Score): string {
  // Band ← src/classify.ts:1  (`Band`)
  type Band = "invalid" | "excellent" | `band-${number}`;

  // classify ← src/classify.ts:8-14  (`classify`)
  function classify(score: Score): Band {
    if (score.value < 0) {
      return "invalid";
    }
    const band = Math.floor(score.value / 10);
    return band >= 9 ? "excellent" : `band-${band}`;
  }

  return `${score.label} is ${classify(score)}`;
}

The shapes travel with the code. Score is named by the target's own signature, so it lands above the function — a parameter annotation cannot see a declaration made inside the body. Band is only classify's business, so it arrives next to classify.


Usage

The package is @hypercrab/roll-in; the command it installs is roll-in.

npm install -g @hypercrab/roll-in
roll-in <file>:<line>:<col>
roll-in <file>:<line>
roll-in <file>#<name>
roll-in <file> <name>

| Option | | | --- | --- | | --depth <n> | How far to follow calls and type references. Default 3. | | --project <p> | tsconfig.json to use. Default: discovered from <file>. | | --out <p> | Write to a file instead of stdout. | | --json | Emit the structured report instead of the rendered function. |

Exit codes: 0 fine, 1 could not resolve the target, 2 bad usage, 3 the emitted text failed its own parse check.


How it decides what to do

For each call it finds, roll-in resolves the callee through the TypeScript type checker and picks one of three outcomes.

Substitute — when it is provably safe. The callee's body must be a single expression, and every argument must be an identifier or a literal, so nothing can be reordered or re-evaluated:

sum += /* src/lib/money.ts:1 */ (Math.round(amount * 100));

Lift — everything else in-project. The callee arrives as a real named definition at the top of the target's body, keeping the form it was written in — a function stays a function, an arrow stays an arrow — and the call site is retargeted to it. Evaluation order, return, await, yield and recursion then all work by construction rather than by transformation.

Refuse — when it cannot be proven. The call is left exactly as written with a one-line reason inline and in the header:

last = /* roll-in: method call, depends on `this` */ tally.add(value);

Anything resolving outside the project contributes its call-site-instantiated type to the header. Its source is never read.

Types

Type aliases and interfaces follow the same walk, bounded by the same --depth. A type the target's signature names is emitted above the target; a type only one lifted helper needs is emitted next to that helper; a type that names other types pulls the whole chain along. References are retargeted just like calls, so a carried Order still reads as Order everywhere — unless the name was already taken, in which case see Names.

Classes, enums, namespaces, and interfaces declared more than once are not carried — each is a value or an ambiguity, not just a shape. They are located in the header with the reason, as are types from outside the project.

Names

Anything carried into the artifact keeps the name it was written under. The one exception is a name that is already taken by the time it arrives — by a binding in the target, by something inside another lifted definition, by an identifier left standing because it was not carried, or by another lifted definition of the same name. Then, and only then, it is emitted as name$1, name$2, and so on:

const RATE = 3;

// RATE$1 ← src/tax.ts:1  (`RATE`)
const RATE$1 = 0.2;

The counter is per name, so a clash between two RATEs says nothing about what any other definition is called. A suffix in the output therefore means exactly one thing: this name refers to something else somewhere above it. The provenance comment above every definition says which file it came from.


Guarantees

  • Never reads source from node_modules, *.d.ts, or outside the project.
  • Never writes to your source tree.
  • Deterministic: same input, byte-identical output.
  • The emitted text is re-parsed before it is returned; parse errors are reported in the header and set exit code 3.
  • Untouched code stays verbatim, comments and all.

Limits

The output is built to be read, not compiled or run. External values and uncarried type names are left dangling by design, module-level mutable state is flagged rather than carried, and lifted constants and types are copies of the originals under new names.

CAVEATS.md is the full list, including what it refuses, what is not implemented, and where it deviates from the spec. Read it before relying on an artifact.


Claude Code skill

This repo doubles as a Claude Code plugin, so an agent can reach for roll-in on its own when a question is about what a TypeScript function actually does across modules — and knows to read the header first, treat refusals as findings, and never mistake the artifact for source.

/plugin marketplace add hypercrab/roll-in
/plugin install roll-in@hypercrab

Or, without the plugin, symlink the skill into your personal skills directory:

ln -s "$PWD/skills/roll-in" ~/.claude/skills/roll-in

skills/roll-in/SKILL.md is the skill; reference.md carries the refusal-code and capture-kind tables it defers to. It resolves a binary through a local install, a global install, $ROLL_IN_HOME, then npx, so it works in a project that has never heard of roll-in.


Development

bun install      # also builds dist/, via `prepare`
bun test         # bun's own runner
bun run typecheck
bun run build    # dist/ — what the package ships and what bin/ runs

The source is written with .ts import specifiers, but the package ships compiled JavaScript in dist/ and bin/roll-in.js imports that. It cannot import src/ directly: Node refuses to strip types for any file under node_modules, so a source entry point runs fine from a checkout and fails under npx, bunx and npm i -g alike.

The test corpus lives in tests/fixtures — 26 self-contained mini-projects, one per semantic case, each with its own tsconfig.json and a case.json describing what it pins down. tests/selfhost.test.ts runs the tool over its own source, which is where most of the real bugs surfaced.

Built on the raw typescript package — ts.createProgram, a custom CompilerHost, and ts.TypeChecker. No AST-manipulation wrapper, no bundler, no second parser.

Pinned to typescript@^6. TypeScript 7 is the native compiler and exposes no JS API — require("typescript") there returns only version — so it is a hard ceiling rather than a pending upgrade. See CAVEATS.md.