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

deslop-js

v0.5.8

Published

Deslop JavaScript code

Readme

deslop-js

version downloads

Deslop JavaScript code.

Finds unused files, dead exports, dead dependencies, circular imports, redundant aliases, duplicate types, and other DRY violations. Each finding carries a confidence tier so you can gate CI on the high-signal ones and treat the rest as code-review prompts.

Install

npm install deslop-js

CLI

The deslop-cli package provides a command-line interface:

npm install -g deslop-cli

Quick start

# scan the current directory
deslop

# scan a specific project
deslop ./my-project

# use the explicit analyze sub-command (equivalent to the above)
deslop analyze ./my-project

Options

deslop [root] [options]

# custom entry points
deslop --entry src/main.ts --entry src/worker.ts

# ignore test files
deslop --ignore "**/*.test.ts" --ignore "**/__mocks__/**"

# only scan specific extensions
deslop --extensions .ts .tsx

# resolve path aliases via tsconfig
deslop --tsconfig ./tsconfig.json

# add explicit path aliases (in addition to the auto-detected ones)
deslop --paths "@app/*=src/*" --paths "@lib/*=packages/lib/*"

# include type-only exports in results
deslop --report-types

# report unused exports from entry files too
deslop --include-entry-exports

# output results as JSON (useful for CI or piping to other tools)
deslop --json

# exit with code 1 when unused code is found (for CI gates)
deslop --fail-on-issues

# exit with code 1 when circular imports are found
deslop --fail-on-cycles

CI example

# fail the build if there are unused exports or circular imports
deslop ./src --fail-on-issues --fail-on-cycles --ignore "**/*.test.ts"

Programmatic Usage

import { analyze, defineConfig } from "deslop-js";

const config = defineConfig({ rootDir: "./my-project" });
const result = await analyze(config);

// unused-code findings (syntactic)
result.unusedFiles; // files unreachable from any entry point
result.unusedExports; // exported symbols never imported
result.unusedDependencies; // package.json deps not imported anywhere
result.circularDependencies; // import cycles

// redundancy / DRY findings (syntactic, on by default)
result.redundantAliases; // `import { x as x }`, useless re-export renames
result.duplicateExports; // same name exported twice from one module
result.duplicateImports; // same specifier imported multiple times
result.redundantTypePatterns; // `T & {}`, `Partial<Partial<T>>`, etc.
result.identityWrappers; // `const wrap = (x) => fn(x)`
result.duplicateTypeDefinitions; // same-shape type declared in N files
result.duplicateInlineTypes; // anonymous `{ a, b, c }` repeated across modules
result.simplifiableFunctions; // `() => { return x }`, `await x; return x`
result.simplifiableExpressions; // `!!x`, `x ? x : y`, `cond ? true : false`
result.duplicateConstants; // same literal value across files
result.crossFileDuplicateExports; // same export name shipped by 2+ files that share an importer
result.reExportCycles; // `export * from "./a"` cycles (self-loop or multi-node)
result.privateTypeLeaks; // exported signature references a non-exported local type

// duplicate-block detection (token-based copy-paste; on by default, disable via `duplicateBlocks.enabled: false`)
result.duplicateBlocks; // suffix-array + LCP detected duplicate code blocks
result.duplicateBlockClusters; // clones grouped by file set + refactoring suggestions
result.shadowedDirectoryPairs; // directory pairs with many identical files

// feature flag inventory (on by default, disable via `featureFlags.enabled: false`)
result.featureFlags; // LaunchDarkly/Statsig/Unleash/PostHog/Vercel Flags/process.env.* uses

// function complexity hotspots (on by default, disable via `complexity.enabled: false`)
result.complexFunctions; // McCabe cyclomatic + SonarSource cognitive per function

// TypeScript-specific smells (on by default)
result.unnecessaryAssertions; // `x as unknown as T`, `x as any`, `x!!`, `<T>x`, `"foo"!`
result.lazyImportsAtTopLevel; // top-level `await import(...)` / `.then(...)` that should be static
result.commonjsInEsm; // `require()`, `module.exports`, `exports.x` inside ESM modules
result.typeScriptEscapeHatches; // `// @ts-ignore`, `// @ts-nocheck`, undocumented `@ts-expect-error`

// semantic findings (type-aware; on by default, disable via `semantic: { enabled: false }`)
result.unusedTypes; // type aliases / interfaces never referenced
result.unusedEnumMembers; // enum members no reference site uses
result.unusedClassMembers; // class members no caller invokes (skips React/Angular lifecycle methods)
result.misclassifiedDependencies; // `dependencies` entries used only as types

// diagnostics
result.analysisErrors; // structured errors from any pipeline stage
result.totalFiles;
result.totalExports;
result.analysisTimeMs;

Programmatic Options

defineConfig accepts a required rootDir and optional overrides:

const config = defineConfig({
  rootDir: "./my-project",
  entryPatterns: ["src/main.ts"],
  ignorePatterns: ["**/*.test.ts"],
  tsConfigPath: "./tsconfig.json",
  reportTypes: true,
  includeEntryExports: true,
  reportRedundancy: true,
  semantic: { enabled: true },
});

| Option | Type | Default | Description | | --------------------- | --------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | rootDir | string | required | Project root directory | | entryPatterns | string[] | auto-detected | Entry point glob patterns | | ignorePatterns | string[] | [] | Glob patterns to exclude from analysis | | includeExtensions | string[] | [".ts", ".tsx", ".js", ".jsx", ".mts", ".mjs", ".cjs", ".cts"] | File extensions to scan | | tsConfigPath | string \| undefined | undefined | Path to tsconfig.json for path alias resolution | | paths | Record<string, string[]> \| undefined | undefined | Explicit path-alias mappings (e.g. { "@app/*": ["src/*"] }), resolved alongside auto-detected aliases | | reportTypes | boolean | false | Include type-only exports in unusedExports | | includeEntryExports | boolean | false | Report unused exports from entry files | | reportRedundancy | boolean | true | Emit the redundancy / DRY findings listed above | | semantic | SemanticConfig | undefined | Opt-in TypeScript type-aware analysis (see below) |

Path aliases are auto-detected by default — from tsconfig paths, Vite (resolve.alias), webpack, Babel (module-resolver), and Jest (moduleNameMapper) configs, plus the workspace layout (a @scope/<dir> import resolves to the matching workspace package even when its package.json name differs). Use paths / --paths only for mappings none of those cover.

Semantic (type-aware) analysis

On by default. Loads the TypeScript program when the project has a valid tsconfig.json; gracefully no-ops on JS-only projects. Disable with semantic: { enabled: false } to skip the ~1–3s program load.

const config = defineConfig({
  rootDir: "./my-project",
  semantic: {
    enabled: true,
    reportUnusedTypes: true,
    reportUnusedEnumMembers: true,
    reportUnusedClassMembers: false, // off by default, noisy on framework code
    reportMisclassifiedDependencies: true,
    reportRedundantVariableAliases: true,
    reportRoundTripAliases: true,
  },
});

| Option | Default | Notes | | --------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | enabled | false | Master switch; semantic analysis loads the TS program and adds ~1–3s per scan | | reportUnusedTypes | true | Type aliases / interfaces / type-only exports never referenced | | reportUnusedEnumMembers | true | Enum members no reference site reads or writes | | reportUnusedClassMembers | false | Subclass overrides, framework method-by-name invocation (@HttpGet, lifecycle hooks) produce too many stylistic FPs to enable by default. Opt in selectively. | | reportMisclassifiedDependencies | true | dependencies packages used only via import type | | reportRedundantVariableAliases | true | Local aliases like const X = Y; export { X } | | reportRoundTripAliases | true | import { X as Y } from "./a"; export { Y as X } |

Duplicate blocks (token-based copy-paste detection)

On by default. Detects maximal duplicated token sequences across files via a suffix array + LCP pass over a normalized AST token stream. Tune thresholds or disable entirely:

const config = defineConfig({
  rootDir: "./my-project",
  duplicateBlocks: {
    enabled: true, // default
    mode: "semantic", // "strict" preserves identifiers/literals; "semantic" (default) blinds them
    minTokens: 50,
    minLines: 5,
    minOccurrences: 2,
    skipLocal: false, // true => only report cross-directory duplicates
  },
});

Surfaces in three result fields:

  • result.duplicateBlocks — every duplicated block group with all its occurrences
  • result.duplicateBlockClusters — duplicate blocks sharing the same file set, plus an extract-function / extract-module refactoring hint
  • result.shadowedDirectoryPairs — directory pairs (e.g. src/ and deno/lib/) whose files mirror each other

Feature flag inventory

On by default. Scans the codebase for every place a feature flag is read and produces a finding per use. The detector recognizes three families:

  1. Env-var flagsprocess.env.X whose name starts with one of the built-in prefixes FEATURE_, NEXT_PUBLIC_FEATURE_, REACT_APP_FEATURE_, VITE_FEATURE_, NUXT_PUBLIC_FEATURE_, ENABLE_, FF_, FLAG_, TOGGLE_ (extend with extraEnvPrefixes).
  2. SDK calls with provider attribution — LaunchDarkly (useFlag/variation/...), Statsig (useGate/checkGate/...), Unleash (isEnabled/getVariant), GrowthBook (isOn/isOff/getFeatureValue), Split (getTreatment), PostHog (useFeatureFlagEnabled/...), ConfigCat, Flagsmith, Optimizely, Eppo, and Vercel Flags (flag() / evaluate() from flags or @vercel/flags).
  3. Config-object accessconfig.features.X style; off by default because it's heuristic. Opt in with detectConfigObjects: true.

Each finding carries name, path, line, column, sdkProvider (when known), and kind: "env-var" | "sdk-call" | "config-object". The detector also tracks the surrounding if / ternary guard span and sets guardsDeadCode: true when an unusedExports finding falls inside that guard — so a flag whose enabled branch contains only dead code lights up immediately.

The actionable angle: cross-reference result.featureFlags with your live flag dashboard.

  • Flags in the dashboard but missing from result.featureFlags → no longer read by the codebase, safe to retire from the platform.
  • Flags in result.featureFlags with guardsDeadCode: true → the guarded code is unreachable, delete both the flag and its body.
const config = defineConfig({
  rootDir: "./my-project",
  featureFlags: {
    enabled: true, // default
    extraEnvPrefixes: ["MYAPP_FF_"],
    extraSdkFunctionNames: ["myCustomFlag"],
    detectConfigObjects: false, // heuristic config.features.x — opt in if you use that pattern
  },
});

Function complexity (cyclomatic + cognitive)

On by default. Reports per-function McCabe cyclomatic and SonarSource cognitive complexity, function size, and parameter count for every function that breaches at least one threshold. Tune the thresholds or disable entirely:

const config = defineConfig({
  rootDir: "./my-project",
  complexity: {
    enabled: true, // default
    cyclomaticThreshold: 10,
    cognitiveThreshold: 15,
    paramCountThreshold: 5,
    functionLineThreshold: 80,
  },
});

TypeScript code smells

On by default. Four families of TypeScript-specific patterns surfaced at high or medium confidence — no extra config required.

result.unnecessaryAssertions; // type assertions that drop type-safety or do nothing
result.lazyImportsAtTopLevel; // dynamic imports at the module top level
result.commonjsInEsm; // CommonJS forms inside ESM modules
result.typeScriptEscapeHatches; // @ts-ignore / @ts-nocheck / undocumented @ts-expect-error

| Finding | Kinds | | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | result.unnecessaryAssertions | redundant-double-assertion (x as unknown as T), assertion-to-any, redundant-non-null-on-literal ("foo"!), double-non-null (x!!), angle-bracket-assertion (<T>x) | | result.lazyImportsAtTopLevel | top-level-await-import, top-level-then-import | | result.commonjsInEsm | require, module-exports, exports-assignment | | result.typeScriptEscapeHatches | ts-ignore, ts-nocheck, ts-expect-error-without-explanation |

ESM detection follows the runtime rules: .mts/.mjs extensions are always ESM, .cts/.cjs are always CommonJS, and other files inherit from the nearest package.json's "type" field.

Findings have confidence tiers

Every redundancy / semantic finding carries confidence: "high" | "medium" | "low". Use "high" for CI gates; "medium" and "low" are best treated as code-review prompts since intent is sometimes unknowable from syntax alone (e.g. ?? null may be required by a typed callback signature).

Error handling

analyze() never throws on a corrupted file, unparseable tsconfig, or missing dependency. Failures surface as analysisErrors: DeslopError[] with structured code, module, severity, and path fields. See errors.ts for the full taxonomy. Errors at severity: "info" (empty files, binary files, minified bundles skipped from redundancy analysis) are informational and do not indicate problems.

Development

pnpm install
pnpm build
pnpm test
pnpm lint
pnpm format

License

MIT