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

filter-scan-dir

v2.1.12

Published

Extremely fast recursive directory crawling and filtering for Node.js.

Readme

filter-scan-dir

License build

Downloads

npm badge

Extremely fast recursive directory crawling and filtering for Node.js. Returns a flat array of file paths. It can scan 1 million files in about half a second with a warm OS cache and fullStat: false. See Performance for the measured setup.

  • Supports super fast concurrent mode in async version.

Install

npm install --save filter-scan-dir

Usage

import { filterScanDir, filterScanDirSync } from "filter-scan-dir";

// sync
console.log(filterScanDirSync({ cwd: "test" }));

// async
console.log(await filterScanDir({ cwd: "test" }));

Filtering

| Option | Purpose | | --- | --- | | ignoreDirs | Skip exact directory basenames at every depth. Accepts a string or array. | | gitignore | Caller-supplied parser for repository and nested .gitignore rules. Disabled when omitted. | | prefilter | Reject entries before lstat. Requires fullStat: true. | | filterExt | Include only matching extensions. | | ignoreExt | Exclude matching extensions. | | filter | Decide whether to include each file. | | filterDir | Skip directories before scanning their children. |

filter and filterDir receive (name, relativeDir, extras). Return true to accept an entry or false to skip it. Directories enter the output only with includeDir: true. Return { stop: true } to stop the scan. With grouping: true, return a string or { group } to choose a result group. Without grouping, the group is ignored and the entry goes to the result. Callbacks must be synchronous. An async callback throws a TypeError before scanning.

Use prefilter when only some entries need full metadata:

const files = await filterScanDir({
  cwd: ".",
  fullStat: true,
  ignoreDirs: ["node_modules", ".git"],
  prefilter: (name, _dir, entry) => entry.isDirectory() || name.endsWith(".ts"),
  filter: (_name, _dir, { stat }) => stat.size < 100_000,
});

prefilter receives (name, relativeDir, Dirent) and runs synchronously. Return false to reject an entry. Rejecting a directory skips its entire subtree. prefilter with fullStat: false throws before scanning, regardless of rethrowError.

The order is ignoreDirs, .gitignore rules, prefilter, extension filters, lstat, then filter or filterDir. Extension filters apply only to non-directory entries. Early rejections avoid lstat, so rejected entries cannot report lstat errors. ignoreDirs matches names, not paths or glob patterns. Symlinks are never followed.

Supply gitignore: contents => ignore().add(contents) with the caller's own ignore package to read .gitignore files from the nearest ancestor repository (a .git directory or file) through the scan root, then from each visited directory. Without an ancestor repository, rules start at the scan root. Patterns keep their directory scope, including anchored paths, directory-only patterns and negations. Ignored directories are pruned before reading their metadata or children. An explicitly requested scan root inside an ignored tree starts a fresh rule scope at the ignored ancestor, so its own files and rules can still be scanned. cwd plus prefix determines that scan root.

This option works in sync and async scans with either fullStat mode. It reads rules anew for each scan and follows the normal rethrowError setting for rule-file read errors. It applies ignore patterns to all entries, including tracked files; it does not consult the Git index, .git/info/exclude, or global Git excludes. .git is not automatically excluded; use ignoreDirs: ".git" when needed. Default scans do not read ignore files.

The scanner has no parser dependency. Any parser returning a GitignoreMatcher can be used: its test(path) method receives a POSIX path relative to that rule file, with a trailing / for directories, and returns { ignored: boolean, unignored: boolean }. Both values are false when no rule matches; unignored: true explicitly overrides an ancestor's match.

Performance

fullStat: false gets entry types from readdir as Dirent objects. This avoids a separate lstat call for every file and directory entry. In our warm-cache tests, it was 2.3× faster on the fynmesh repo than fullStat: true. It was 8.1× faster on the 1,000 × 1,000 synthetic tree.

Use it when names and entry types are enough:

const files = await filterScanDir({ cwd: "src", fullStat: false });

Filter callbacks receive Dirent objects instead of Stats objects. Sizes, timestamps, and permissions are unavailable.

The default concurrency is 50. Higher concurrency is not always faster. concurrency: Infinity removes the limit. More concurrent reads can increase memory use. Benchmark your directory tree before changing it. filterScanDirSync blocks the event loop.

Keep Dirent mode for filters that only need names or entry types:

const files = await filterScanDir({
  cwd: ".",
  fullStat: false,
  filterExt: [".js", ".ts"],
  ignoreDirs: ["node_modules", ".git"],
});
  • filterDir can skip entire subtrees.
  • Extension filters still read every visited directory. Rejected entries skip lstat in full-stat mode.
  • Custom callbacks add work per entry.
  • sortFiles: true adds sorting work per directory.

Use the default fullStat: true for full metadata. The extra lstat calls can make scans much slower. The cost is higher on cold caches or slow storage.

Scan times depend on storage, directory layout, and OS cache state. Warm-cache measurements do not predict cold-cache performance. Scanning lists entries without reading file contents.

Example measurements

These are medians from seven warm-cache runs. Both modes used concurrency 50. No filters or sorting were enabled. Symlinks were excluded.

Test system: Node.js 22.22.2 on macOS, Apple M4 Pro, 24 GB RAM.

| Tree | Files | Async fullStat: false | Async fullStat: true | | --- | ---: | ---: | ---: | | Synthetic: 1,000 directories with 1,000 empty files each | 1,000,000 | 551 ms | 4,483 ms | | fynmesh repo with installed dependencies: 48,347 directories | 292,242 | 949 ms | 2,157 ms |

The fynmesh repo scan included node_modules and .git. Its directory count includes the root.

License

Copyright (c) 2022-2026 Joel Chen

Licensed under the Apache License, Version 2.0