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

@flagshark/core

v2.8.0

Published

Detection engine for FlagShark — finds feature flag references across 13 languages.

Readme

@flagshark/core

The detection engine behind FlagShark — finds feature flag references across 13 languages and 13 providers, and identifies stale flags via git blame.

This is the library. For a CLI or GitHub Action, install flagshark instead.

📚 Library documentation: flagshark.com/docs/getting-started/library

Install

npm install @flagshark/core

Quick start

import { scanRepo } from '@flagshark/core'

const result = await scanRepo({
  cwd: process.cwd(),
  threshold: 30, // days — flag is stale if older than this
})

console.log(`${result.totalFlags} flags, ${result.staleFlags.length} stale`)
console.log(`Health score: ${result.healthScore}/100`)
console.log(`Languages: ${Object.keys(result.languageBreakdown).join(', ')}`)

Detection

For tier-1 languages (TypeScript, JavaScript, Go, Python), detection uses tree-sitter (via WASM) to parse files into a real AST. That eliminates the false-positive class where regex matched flag-shaped strings inside comments, string literals, or unrelated call expressions. It also handles multi-line calls and resolves const-bound flag keys (const FLAG = 'X'; client.variation(FLAG, …)) in TS/JS.

The remaining 9 languages currently use regex-based detection; each migrates to tree-sitter in future releases (no API break).

The engine is a runtime concern, not a public API surface. Consumers calling scanRepo() or createDefaultRegistry() get the right engine per-language automatically. Override per-detector if needed (see below).

Bundling and WASM resolution

Detection has two runtime asset dependencies: the four tree-sitter grammar WASMs (~2.5 MB total, binary, must be external) and the four .scm query files (~1.4 KB total, text, inlined into the JS bundle). The .scm files come along for free because we ship them as TypeScript string constants — there is nothing for you to copy, no env var to set, no resolution path that can break. Only the WASMs need attention.

For the WASM grammars (tree-sitter-typescript.wasm, tree-sitter-javascript.wasm, tree-sitter-go.wasm, tree-sitter-python.wasm), parser-cache calls createRequire(...).resolve(...) against node_modules/tree-sitter-*. The four grammar packages are declared as runtime dependencies of @flagshark/core — under normal npm install / bun install use you don't need to do anything; the grammars sit next to your node_modules copy of @flagshark/core and resolution Just Works.

If you're bundling @flagshark/core into a Lambda, container, edge function, or any other distributable artifact, three resolution paths exist:

1. Node target, grammars external (recommended). Mark the four tree-sitter-* packages as external in your bundler and install them next to the bundle. AWS CDK's lambdaNodejs.NodejsFunction does this via nodeModules: ['tree-sitter-typescript', …]; esbuild does it via external: [...] plus a side-by-side package.json. The grammars stay on disk in node_modules/tree-sitter-* and parser-cache resolves them through __filename (CJS bundles) or import.meta.url (ESM bundles).

2. Node target, grammars flattened. If your bundling pipeline puts grammars in a custom location, set FLAGSHARK_WASM_DIR to that directory and parser-cache will read ${FLAGSHARK_WASM_DIR}/<basename>.wasm directly — bypassing require.resolve entirely. The four files must be there with their exact tree-sitter-<lang>.wasm names. This is what the GitHub Action uses (packages/action/scripts/build.mjs copies them at build time and the entry point exports FLAGSHARK_WASM_DIR before importing the engine).

3. Non-Node target (edge runtimes, browsers). Same as path 2: ship the four WASM files alongside your bundle and set FLAGSHARK_WASM_DIR to that directory. There is no working "do nothing" path here — createRequire doesn't exist on Workers/Deno/browsers, and the tree-sitter engine has to find the grammars somehow.

If neither import.meta.url nor __filename is available and FLAGSHARK_WASM_DIR is unset, parser-cache throws with a pointer at this section.

FLAGSHARK_QUERIES_DIR also exists as an escape hatch for consumers who want to swap out the inlined queries at runtime (custom detection rules, vendored variants). When set, loadQueryText reads ${dir}/${lang}.scm from disk instead of using the inlined string. Most consumers will never need this — the inlined defaults are the same .scm files we publish to dist/detection/tree-sitter/queries/.

Visibility into detection failures

Every analysis run that produces parse errors emits a deduplicated sample at WARN, alongside the existing Polyglot analysis completed summary. If grammar resolution breaks, a WASM file is missing, or a detector throws, you see the underlying error message in the log — not just a filesWithErrors: N counter. The per-file try/catch in analyzeFile stays (one bad file shouldn't fail an entire scan), but the errors no longer disappear into a counter.

Pass any ScanLogger to new PolyglotAnalyzer(registry, logger) and the sample lands on logger.warn('Parse errors during analysis', { uniqueErrors, sample }). The top 5 unique error messages are reported with up to 3 example file paths each.

API

scanRepo(options) → Promise<ScanRepoResult>

Top-level orchestrator. Walks a checked-out repository, detects flags, runs staleness analysis, and returns a summary.

interface ScanRepoOptions {
  /** Absolute path to the repository being scanned. */
  cwd: string

  /** Staleness threshold in days. Overridden by `config.threshold` if config is supplied. */
  threshold?: number

  /** If set, only scan files changed since this git ref (e.g. `origin/main`). */
  diff?: string

  /** Optional cancellation signal. Cancels file analysis only;
   *  `git blame` subprocesses always run to completion. */
  signal?: AbortSignal

  /** Optional `{ debug, info, warn, error }` logger. Defaults to no-op. */
  logger?: ScanLogger

  /** Explicit FlagsharkConfig. If undefined, scanRepo auto-discovers
   *  `.flagshark.yml` walking upward from `cwd`. */
  config?: FlagsharkConfig

  /** Skip `.flagshark.yml` discovery entirely. */
  noConfig?: boolean

  /** Skip `.flagsharkignore` discovery entirely. */
  noIgnoreFile?: boolean

  /** Populate `result.excludedPaths` with the full list of skipped file paths.
   *  Off by default — counts only. */
  collectExcludedPaths?: boolean
}

interface ScanRepoResult {
  totalFlags: number
  staleFlags: StaleFlag[]
  detectedProviders: string[]
  languageBreakdown: Record<string, number>
  healthScore: number              // 0–100
  scanDuration: number             // ms
  excludedCount?: number           // files skipped by config + .flagsharkignore
  excludedPaths?: string[]         // set when collectExcludedPaths: true
  effectiveExcludes?: EffectiveRules  // for debug/verbose output
}

scanRepo walks the local filesystem. If you can't run a git checkout (e.g., a serverless function reading from an API), use the lower-level primitives below.

Config module

The same config used by the CLI is exported as a building block:

import {
  buildDefaultConfig,
  loadConfigFile,
  loadIgnoreFile,
  buildExcluder,
  expandPresets,
  PRESETS,
  FlagsharkConfigSchema,
  type FlagsharkConfig,
} from '@flagshark/core'

// Load a .flagshark.yml from disk (validated via zod)
const loaded = await loadConfigFile('/path/to/repo')
const config = loaded?.config ?? buildDefaultConfig()

// Load a .flagsharkignore (separate file at repo root)
const ignoreFile = await loadIgnoreFile('/path/to/repo')

// Build a unified excluder from all sources
const excluder = buildExcluder({
  config,
  ignoreFilePatterns: ignoreFile?.patterns ?? [],
})

// Use it directly:
excluder.shouldExclude('examples/demo.ts')  // true

The schema accepts:

threshold: 30
excludes:
  paths: []        # gitignore-style globs
  files: []        # same — split is purely organizational
  presets: []      # ['test-files', 'snapshots', 'examples', 'stories', 'fixtures', 'generated']
suppress:
  flags: []        # flag-name globs (post-detection filter)
paths:
  - match: 'src/critical/**'
    threshold: 90
providers: []      # custom provider definitions (see Custom Detectors)

Lower-level primitives

For consumers that fetch file contents elsewhere (GitHub API, S3, in-memory, etc.):

import { createDefaultRegistry, PolyglotAnalyzer } from '@flagshark/core'

const registry = createDefaultRegistry()           // 13 detectors pre-registered
const analyzer = new PolyglotAnalyzer(registry, logger)

// `files` is a Map<filePath, content>
const result = await analyzer.analyzeFiles(files)
//   .totalFlags: Map<flagName, FeatureFlag[]>
//   .languages:  Map<Language, number>

For staleness analysis with a local checkout:

import { analyzeStaleness } from '@flagshark/core'

const stale = await analyzeStaleness(result.totalFlags, {
  thresholdDays: 30,
  repoRoot: '/path/to/repo',
})

Custom detectors

createDefaultRegistry() returns a LanguageRegistry. Register additional detectors that implement the LanguageDetector interface:

import { createDefaultRegistry, LanguageDetector } from '@flagshark/core'

const registry = createDefaultRegistry()
registry.register(new MyCustomDetector())

Or import individual detectors and compose your own registry. Each tier-1 detector accepts { engine: 'regex' | 'tree-sitter' }:

import {
  LanguageRegistry,
  TypeScriptDetector,
  GoDetector,
  type DetectorEngine,
} from '@flagshark/core'

const registry = new LanguageRegistry()
registry.register(new TypeScriptDetector({ engine: 'tree-sitter' }))
registry.register(new GoDetector({ engine: 'regex' }))  // opt back to regex

Supported languages

TypeScript, JavaScript, Go, Python, Java, Kotlin, Swift, Ruby, C#, PHP, Rust, C/C++, Objective-C.

Supported providers

Auto-detected from imports — no configuration needed:

LaunchDarkly · Unleash · Flipt · Split.io · PostHog · Flagsmith · ConfigCat · Statsig · GrowthBook · DevCycle · Eppo · Optimizely · plus generic custom-flag patterns.

How detection works

FlagShark only scans files that actually import a flag SDK. A function called isEnabled() in a file that doesn't import LaunchDarkly/Unleash/etc. won't be flagged — this prevents false positives from generic identifier names.

Once a file qualifies, the engine (tree-sitter for tier-1, regex for the rest) walks call expressions and extracts the flag-key argument at the configured position for each provider's method signatures. Provider attribution and source location come along automatically.

How staleness works

A flag is marked stale if any signal fires:

  1. agegit blame shows the flag's line was last modified more than threshold days ago.
  2. low-usage — The flag name appears in only one file across the repo, suggesting a completed rollout.

License

MIT