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

@lindorm/scanner

v0.6.0

Published

Recursive filesystem scanner that turns a directory tree into structured metadata and dynamically imports the discovered modules.

Readme

@lindorm/scanner

Recursive filesystem scanner that turns a directory tree into structured metadata and dynamically imports the discovered modules.

This package is ESM-only. All examples use import; require is not supported.

Installation

npm install @lindorm/scanner

Features

  • Walks a directory tree synchronously and returns a hierarchical IScanData tree.
  • Each node carries baseName, basePath, extension, parents, relativePath, types, and children.
  • Splits a filename like repo.controller.ts into baseName: "repo", extension: "ts", types: ["controller"] (additional dot-separated segments become entries in types, ordered last-to-first).
  • Filters traversal with regex deny lists for directories, extensions, filenames, and dot-separated type segments.
  • Loads any scanned file via import(), normalising the returned namespace across runtimes (native ESM, tsx, vitest, ts-jest vm-modules) so consumers always see the user's real named and default exports.
  • Falls back to tsx/esm/api when the host runtime cannot load a .ts file (e.g. Node without a loader, or a transformer that does not support TC39 stage-3 decorators).
  • Static helpers to flatten the tree and check whether a directory contains entries.

Usage

Scan a directory

import { Scanner } from "@lindorm/scanner";

const scanner = new Scanner({
  deniedDirectories: [/^\.git$/, /^node_modules$/],
  deniedExtensions: [/^map$/],
  deniedFilenames: [/^index$/],
  deniedTypes: [/^test$/, /^spec$/],
});

const tree = scanner.scan(`${import.meta.dirname}/routes`);
const files = Scanner.flatten(tree);

for (const file of files) {
  console.log(file.relativePath, file.types);
}

Import a scanned module

import { Scanner } from "@lindorm/scanner";

const scanner = new Scanner();
const tree = scanner.scan(`${import.meta.dirname}/plugins`);

const loadPlugin = async (file: { fullPath: string }) => {
  const mod = await scanner.import<{ default: unknown }>(file);
  return mod.default;
};

for (const file of Scanner.flatten(tree)) {
  if (file.extension !== "ts" && file.extension !== "js") continue;
  await loadPlugin(file);
}

import accepts either an IScanData node or an absolute path string. The returned object exposes the source module's exports directly — no __esModule or module.exports wrappers.

Check whether a directory has any entries

import { Scanner } from "@lindorm/scanner";

if (Scanner.hasFiles(`${import.meta.dirname}/migrations`)) {
  // …
}

API

class Scanner

new Scanner(options?: StructureScannerOptions)

All options are optional. Each is an array of RegExp tested against the candidate string; a single match excludes the node.

| Option | Tested against | | ------------------- | -------------------------------------------------------------------- | | deniedDirectories | The directory's basename. | | deniedExtensions | The file extension without leading dot (e.g. "ts", "json"). | | deniedFilenames | The file's baseName (the segment before the first dot). | | deniedTypes | Each dot-separated segment between the baseName and the extension. |

scanner.scan(path: string): IScanData

Synchronously walks path. Returns the root IScanData node. Throws ScannerError("No files found") if the root is filtered out by the configured deny lists.

scanner.import<T>(fileOrPath: IScanData | string): Promise<T>

Dynamically imports the file referenced by fileOrPath.fullPath (or the string path itself). Tries native import() first; if that throws ERR_UNKNOWN_FILE_EXTENSION or a SyntaxError on a .ts/.tsx/.mts/.cts file, retries via tsx/esm/api. The resolved value is the module's exports with any runtime-specific interop wrappers stripped.

Scanner.flatten(scan: IScanData | Array<IScanData>): Array<IScanData>

Static. Depth-first walk that returns every isFile: true descendant in a flat array. Directory nodes are dropped from the output.

Scanner.hasFiles(directory: string): boolean

Static. Returns true if readdirSync(directory) yields at least one entry. Returns false on any error (missing directory, permission denied, etc.).

class ScanData implements IScanData

Plain data class. new ScanData(options: IScanData) copies every field from options onto the instance. The fields below match IScanData.

interface IScanData

interface IScanData {
  baseName: string;
  basePath: string;
  children: Array<IScanData>;
  extension: string | null;
  fullName: string;
  fullPath: string;
  isDirectory: boolean;
  isFile: boolean;
  parents: Array<string>;
  relativePath: string;
  types: Array<string>;
}

For repo.controller.ts scanned from a root that contains it under src/users/, the values are: baseName: "repo", basePath: "src/users/repo.controller", extension: "ts", fullName: "repo.controller.ts", parents: ["src", "users"], relativePath: "src/users/repo.controller.ts", types: ["controller"].

interface IScanner

interface IScanner {
  scan(path: string): IScanData;
  import<T>(fileOrPath: IScanData | string): Promise<T>;
}

type StructureScannerConfig / type StructureScannerOptions

type StructureScannerConfig = {
  deniedDirectories: Array<RegExp>;
  deniedExtensions: Array<RegExp>;
  deniedFilenames: Array<RegExp>;
  deniedTypes: Array<RegExp>;
};

type StructureScannerOptions = Partial<StructureScannerConfig>;

class ScannerError extends LindormError

Thrown by scan when no files match. Inherits from @lindorm/errors's LindormError.

License

AGPL-3.0-or-later.