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

@devmedic/parser-typescript

v0.1.0

Published

Language adapter: TypeScript/JavaScript source to unified AST.

Readme

@devmedic/parser-typescript

The AST Engine: parsing, a single-pass visitor system with plugin hooks, and format-preserving printing for TypeScript, TSX, JavaScript, and JSX. No rules live here — this is parser infrastructure only.

import { parseFile, walk } from '@devmedic/parser-typescript';

const { program } = parseFile({ filename: 'app.tsx', source });

const state = { componentNames: [] };
walk(
  program,
  [
    {
      name: 'find-components',
      visitor: {
        FunctionDeclaration(path, state) {
          if (/^[A-Z]/.test(path.node.id?.name ?? '')) {
            state.componentNames.push(path.node.id.name);
          }
        },
      },
    },
  ],
  { state },
);

The four tools, and why each one

| Tool | Role | Why it's a separate module | | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | | Babel Parser (@babel/parser) | The default, fast parse path (parse.ts) — syntax only, no type-checking. | parseFile() is what traversal/analysis should use; it's what makes the engine fast. | | Babel Traverse (@babel/traverse) | The visitor system (visitor.ts) — merges every registered plugin's visitor into one traversal via Babel's own visitors.merge. | This is the single-pass mechanism Babel itself uses to run many plugins together; no reason to reinvent it. | | TypeScript Compiler API (typescript) | Opt-in semantic analysis (typescript-program.ts) — a real ts.Program/TypeChecker when a caller actually needs type information. | Full type-checking is meaningfully more expensive than a syntax-only parse; keeping it separate keeps the default path fast. | | Recast | Format-preserving parse/print (printing.ts) — for the future Auto Fix Engine, where a fixer needs to reprint only what it changed. | Printing needs different parser settings (tokens: true) than analysis does; conflating the two would slow down the common case. |

Supported dialects

.ts/.mts/.cts, .tsx, .js/.mjs/.cjs, .jsx — inferred from the filename, or pass language explicitly. .ts never enables JSX syntax (it isn't legal there); everything else does.

The visitor system / plugin hooks

An AstPlugin is just { name, visitor } — a named Babel visitor. The engine doesn't know or care what a plugin does; mergeAstPlugins/walk only merge and run them. A future rule pack is nothing more than a plugin that happens to report findings — that's deliberately out of scope here.

Node-visit counting (for benchmarking)

beginNodeVisitCount() / endNodeVisitCount() arm and read an opt-in, global counter of every node any walk() call visits in between — zero cost when not armed (no extra plugin is merged in). This exists because walk() is the one traversal choke point every rule goes through (directly or via a thin wrapper); it's how @devmedic/rule-benchmark measures real "AST nodes visited" per rule without every rule needing its own instrumentation.

import { beginNodeVisitCount, endNodeVisitCount } from '@devmedic/parser-typescript';

beginNodeVisitCount();
rule.analyze(context); // internally calls walk() one or more times
const nodesVisited = endNodeVisitCount();

Being global (not per-walk()-call) state, only one measurement should be in flight at a time — concurrent analyze() calls while armed would mix their counts.

Performance

  • Parallel-in-spirit, not parallel-in-threads: within a single file, the fast path never does more work than one syntax parse + one merged traversal.
  • Single-pass traversal: N plugins still walk the tree once, via @babel/traverse's own multi-visitor merge — not N tree-walks.
  • Type-checking is opt-in: createTypeScriptProgram is a separate function nothing else calls implicitly.
  • Printing is separate from analysis: parseFile never carries the tokens: true overhead parseForPrinting needs for format preservation.

A real bug this caught

The first build of walk() passed all 33 unit tests under vitest but threw TypeError: traverse is not a function when actually run via node dist/index.js. Vitest's esbuild-based transform auto-unwraps @babel/traverse's CJS default export; plain Node's native ESM/CJS interop does not — it hands you the whole module.exports object instead, with the real function nested one level deeper at .default. visitor.ts now resolves this defensively at runtime (works either way), and runtime-interop.test.ts runs the actual compiled dist/ output in a real node subprocess so this class of bug can't silently come back.

Depends on

  • @devmedic/core
  • @babel/parser, @babel/traverse, @babel/types
  • typescript
  • recast