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

custom-biome-lint

v0.4.5

Published

Standalone linting tool for Reselect/Redux patterns not in Biome

Readme

custom-biome-lint

Standalone linter for the Reselect / Redux / Immutable patterns that Biome does not cover. It exists so the three remaining custom ESLint rules in this codebase can be retired without losing their coverage during the Biome migration.

Written in Rust on Biome's own JS parser, so it sees the same AST Biome does and handles JSX inside .js files.

The tool is fully standalone and project-agnostic: it is published to npm as a public package that any JavaScript/JSX project can adopt. The example file paths shown throughout this README (e.g. src/components/..., src/selectors/...) are illustrative only.

Rules

Every rule is enabled and reports at error severity by default except the six marked off by default — those encode a house style (banning loops, deep parameter mutation) rather than a universal correctness fix, so a repo must opt in. See Configuration for how to flip any rule's state or severity.

| Rule | Default | What it catches | | --- | --- | --- | | no-native-map | on | new Map() where Immutable.js Map is expected. Understands import { Map } from 'immutable', import Immutable from 'immutable', require('immutable'), const { Map } = Immutable and Immutable.Map aliases. | | no-arrow-function-create-selector | on | createSelector wrapped in an arrow function, which rebuilds the selector on every call and defeats memoization. Names matching /^make[A-Z]/ are treated as deliberate factories and allowed. | | reselect-arity-match | on | A createSelector result function whose parameter count does not match the number of input selectors. | | destructure-default-param-assign | on | Reassigning a parameter binding that came from destructuring — function f({ b }) { b = 'x' }. Biome's noParameterAssign only tracks plain identifier parameters. | | destructure-param-prop-assign | on | Mutating a property of a destructured parameter at any depth — ({ state }) => { state.tours[id].bands = {} }. Biome's equivalent check tracks one level. | | bare-arrow-param-prop-assign | off | Property mutation through an arrow's unparenthesized single parameter — d => { d.token = 'x' } — an AST shape Biome's noParameterAssign does not see. | | deep-param-prop-assign | off | Plain-parameter mutation 2+ levels deep — function f(acc) { acc[x][y] = 1 } — beyond Biome's one-level limit. | | no-for-statement | off | Classic three-clause for (init; test; update) { ... } loops. Part of the "no loops, use functional iteration" house style; for...of / for...in are out of scope. | | no-while-statement | off | while (...) { ... } loops. Same house style as the loop bans. | | no-do-while-statement | off | do { ... } while (...) loops. Same house style as the loop bans. | | param-mutating-array-method-call | off | Array-mutating method calls (push, pop, shift, unshift, splice, sort, reverse, fill, copyWithin) on a parameter — param.push(item). Name-based; companion to the parameter-mutation rules. |

The first three on rules are direct ports of the corresponding rules in eslint-rules/, deliberately behaviour-for-behaviour rather than "improved", so that enabling this tool produces exactly the findings ESLint produced.

The parameter-mutation and loop-ban rules are not ports: they close measured gaps between ESLint's removed no-param-reassign / no-restricted-syntax and Biome's built-in checks. The six marked off-by-default only run once a repo opts in via ignoreBiomeExtensionRules.

Full details, including known false positives and non-goals, are in docs/RULES.md.

Installation

npm install custom-biome-lint
npx custom-biome-lint --help

Rust is NOT required for normal npm installation or usage. npm install pulls down a small JS launcher plus a precompiled native binary for your platform via npm's optionalDependencies — the same distribution model Biome, esbuild, and swc use. See docs/DISTRIBUTION.md for how it works under the hood.

Supported platforms

| OS | Architecture | libc | | --- | --- | --- | | macOS | ARM64 | — | | macOS | x64 | — | | Linux | ARM64 | glibc | | Linux | ARM64 | musl (Alpine) | | Linux | x64 | glibc | | Linux | x64 | musl (Alpine) | | Windows | ARM64 | — | | Windows | x64 | — |

Linux has one package per libc flavor because a glibc-linked binary cannot run on musl at all. The launcher detects which one this machine needs at run time; CUSTOM_BIOME_LINT_LIBC=musl|glibc overrides that detection if it ever gets it wrong. See docs/DISTRIBUTION.md#linux-glibc-vs-musl.

If your platform isn't listed, npx custom-biome-lint prints a clear error naming your platform/arch (and libc, on Linux) and the supported list — it never falls back to compiling from source.

IDE integration

The custom-biome-lint binary is the engine behind the in-editor linting and quick-fix experience for these custom rules:

  • VS Code — the Comment Doc Links extension invokes the binary and turns its diagnostics into editor markers, with code actions to apply a safe fix or insert a custom-biome-ignore-* suppression comment.
  • JetBrains / WebStorm — the companion plugin drives the same binary, so both editors consume one contract and never drift.

Both adapters read the machine-readable --format json output (protocol v1) and apply the fixes[] / suppressions[] edits the binary reports. The binary is the single source of truth for what a fix or suppression does — the editors never compute edit placement themselves.

Building from source (contributors, git submodule consumers)

Rust is required for this path. See docs/USE_AS_GIT_SUBMODULE.md and "Build" below.

Documentation

| Document | Contents | | --- | --- | | docs/SETUP.md | Installing Rust from zero, building, running the binary | | docs/ARCHITECTURE.md | Module walkthrough and why each non-obvious decision was made | | docs/RULES.md | Each rule with before/after examples, known quirks, real-codebase findings | | docs/ADDING_A_RULE.md | Step-by-step guide with a worked example | | docs/TESTING.md | Test suites, clippy, fixture and real-tree runs, portability check | | docs/CI_CD_INTEGRATION.md | Husky and GitLab CI wiring (not yet applied) | | docs/MIGRATION_NOTES.md | The 8 suppression comments still to translate | | docs/INCREMENTAL_CACHING_DOCUMENT.md | How the content-hash cache works, and why it replaced mtime | | docs/BENCHMARKING.md | Re-runnable performance harness (scripts/benchmark.sh) and current numbers | | docs/SEMANTIC_MODEL.md | Lexical scope/binding model and identifier resolution: design, limitations, how the rules use it | | docs/DISTRIBUTION.md | How the precompiled-binary npm packages work: platform packages, the JS launcher, the release pipeline | | docs/PUBLISH_TO_NPM.md | Publishing/version-bump procedure for the main package and the 8 platform packages |

Build

Building the Rust binary yourself is only needed for source development or the git submodule workflow — normal npm install custom-biome-lint consumers never need this (see Installation above).

cargo build --release        # or: npm run build:native (npm run build is an alias)

The binary lands at target/release/custom-biome-lint. For an unoptimized, faster-to-compile build during iteration, use npm run dev:build (cargo build), which lands at target/debug/custom-biome-lint.

Usage

custom-biome-lint [PATTERN] [FLAGS]

PATTERN is a glob, defaulting to src/**/*.{js,jsx}. *, ?, ** and {a,b} brace sets are supported. A bare directory is expanded for you, so custom-biome-lint src means src/**/*.{js,jsx}.

custom-biome-lint                          # lint src/**/*.{js,jsx}
custom-biome-lint 'src/store/**/*.js'      # narrow the scope
custom-biome-lint src/reducers             # bare directory shorthand
custom-biome-lint -v                       # show config, rules and pattern

Quote globs so your shell does not expand them first.

Flags

| Flag | Effect | | --- | --- | | --write-fix | Add a suppression comment for every violation, in place | | --auto-fix | Rewrite violations in place using each rule's own fix; rules with no unambiguous fix are reported as skipped. Cannot combine with --write-fix. | | --dry-run | With --write-fix or --auto-fix, report the changes without writing | | --format <text\|json> | Diagnostics output format (default: text). Not supported with --write-fix or --auto-fix. | | -v, --verbose | Config source, enabled/skipped rules, resolved pattern | | -vv | Brace expansion, walk root, discovery counts | | -vvv | Per-file: rules run, violation count, line count | | -d, --debug | Internal state and every step (outranks -vvv) | | --trace | Prefix each log line with its source location | | -h, --help | Usage | | -V, --version | Version |

Diagnostics go to stdout; all logging and warnings go to stderr, so custom-biome-lint > report.txt captures just the report.

Exit codes

| Code | Meaning | | --- | --- | | 0 | No violations (with --write-fix: every violation was suppressed; with --auto-fix: every violation was fixed) | | 1 | Violations found, or a violation could not be suppressed/fixed | | 2 | Bad usage, or the pattern's root directory does not exist |

A --write-fix --dry-run or --auto-fix --dry-run run that has anything to write also exits 1, so either works as a CI check.

Output

ESLint's format — a path header, aligned line:col severity message rule rows, then a summary:

src/components/Example.jsx
  120:30  error  Use Immutable.js Map instead of native Map.  no-native-map

src/selectors/example.js
  12:64  error  createSelector expects 1 parameter(s) in the result function, but found 2.  reselect-arity-match

✖ 2 errors in 2 files

JSON output

--format json prints a single stable JSON document to stdout instead — no other stdout content in that mode, so a consumer can parse it directly:

custom-biome-lint --format json > report.json
{
  "version": 1,
  "files": [
    {
      "path": "src/selectors/example.js",
      "violations": [
        {
          "line": 12,
          "col": 64,
          "severity": "error",
          "rule": "reselect-arity-match",
          "message": "createSelector expects 1 parameter(s) in the result function, but found 2."
        }
      ]
    }
  ],
  "summary": {
    "errors": 1,
    "warnings": 0,
    "filesWithViolations": 1,
    "filesChecked": 9,
    "filesCacheSkipped": 0,
    "elapsedMs": 7,
    "clean": false
  }
}

The schema is additive-only across versions: existing fields never change meaning or disappear, so a consumer that reads only the fields it knows about keeps working after an upgrade.

filesCacheSkipped counts discovered files the incremental cache found already valid (unchanged content, same enabled rules and tool version) and so never re-analyzed this run — see docs/INCREMENTAL_CACHING_DOCUMENT.md for why filesChecked: 0 with a nonzero filesCacheSkipped is a correct, healthy result, not a sign the run did nothing. The text summary shows the same thing as a , N skipped via cache clause, but only when nonzero — an uncached (or all-cache-miss) run's output is unchanged from before this field existed: ✔ No violations found (0 files checked in 5ms). A run where the cache skipped files adds the clause: ✔ No violations found (0 files checked, 3 skipped via cache in 5ms).

Configuration

Rule severities are set by name in the nearest package.json at or above the working directory, via ignoreBiomeExtensionRules. Two shapes are accepted:

{
  "ignoreBiomeExtensionRules": ["no-native-map"]
}

The array form is shorthand for turning listed rules fully "off". For finer control, use the object form with "off" / "warn" / "error" per rule:

{
  "ignoreBiomeExtensionRules": {
    "no-native-map": "off",
    "reselect-arity-match": "warn"
  }
}

"off" disables the rule entirely, same as the array form. "warn" and "error" don't change whether the rule runs — only the severity of what it reports. "warn" violations are still printed and still counted, but (unlike "error", the default) they don't make the run exit non-zero, so a rule you want visibility into without blocking CI can be turned down without silencing it. A rule with no entry keeps its default severity ("error").

A missing package.json is not an error — every rule stays enabled at its default severity.

Suppressions

Two comment forms:

const cache = new Map(); // custom-biome-ignore-line no-native-map

// custom-biome-ignore-next-line no-native-map, reselect-arity-match
const other = new Map();

Comma- and space-separated names both work. Text after a -- token is ignored, so a justification can be written inline:

const nodeCache = new Map(); // custom-biome-ignore-line no-native-map -- keys are DOM nodes

A marker with no rule names suppresses every rule on its target line:

const anything = new Map(); // custom-biome-ignore-line

Prefer naming the rule. A bare marker also hides rules added later, and rules that start firing on that line for unrelated reasons, so it trades away the warning you would otherwise get.

Inside JSX children a // comment is rendered text, not a comment, so the brace form is required there — and is what --write-fix emits:

<div>
  {/* custom-biome-ignore-next-line no-native-map */}
  {new Map().get(key)}
</div>

A marker only counts inside a real comment; the same text in a string literal is not a suppression. Only the first marker on a line is parsed, so a line cannot carry two markers. Suppressions apply to the line the violation is reported on, which for reselect-arity-match is the line of the result function, not necessarily the createSelector call.

Adding suppressions automatically

--write-fix adds a suppression comment for every violation it finds, which is how an existing codebase is brought to a clean baseline:

custom-biome-lint --write-fix --dry-run src   # report what would change
custom-biome-lint --write-fix src             # apply it

Placement rules:

  • a trailing custom-biome-ignore-line when the resulting line stays within 100 columns, otherwise custom-biome-ignore-next-line on its own line above, indented to match;
  • the {/* ... */} form, always on its own line, when the insertion point is in JSX children — a trailing brace comment there would leave a whitespace-only text node that React renders as a space;
  • several violations on one line share a single comment;
  • an existing suppression comment is extended with the missing rule names rather than duplicated, so re-running is idempotent.

Anything that cannot be suppressed without risking a change in meaning is left alone and reported as a warning: a line inside a multi-line template literal or block comment, and any file with parse errors. Every rewrite is re-parsed and re-checked before it is written, and --write-fix exits non-zero if any violation was left unsuppressed.

Fixing violations instead of suppressing them

--auto-fix rewrites the flagged code itself, using the exact edit the rule that detected the violation produced — a different mechanism from --write-fix, which never touches the flagged code and only adds a comment around it:

custom-biome-lint --auto-fix --dry-run src   # report what would change
custom-biome-lint --auto-fix src             # apply it

Only a rule with one unambiguous correction produces a fix; a rule where the correction would have to guess (e.g. reselect-arity-match, which cannot know whether the selector list or the result function is the one that's wrong) leaves it None and its violations are reported as skipped rather than guessed at. Currently:

| Rule | Fix | | --- | --- | | no-arrow-function-create-selector | Unwraps the arrow, keeping the createSelector(...) call as-is | | no-native-map | None — Map occurrences it flags include known false positives (see ESLint parity below), so there is no always-safe rewrite | | reselect-arity-match | None — the fix would have to guess which side of the mismatch is wrong |

As with --write-fix, every rewrite is re-parsed before it is written, and --auto-fix exits non-zero if anything was left unfixed. --write-fix and --auto-fix cannot be combined in one run — apply one, look at the result, then the other if you want both.

ESLint parity

These rules reproduce their ESLint counterparts' findings exactly. One consequence is worth knowing up front: no-native-map flags any identifier named Map, including member names, so new mapboxgl.Map({...}) is reported. That is faithful to the original rule, not a port bug, and it is why eight call sites in this codebase already carry disable comments.

Those eight comments need translating to this tool's syntax — the tool deliberately does not honour eslint-disable* comments. See docs/MIGRATION_NOTES.md for the exact diff and the reasoning, and docs/RULES.md for each rule's limitations.

Adding a rule

  1. Create src/rules/my_rule.rs and implement Rule.
  2. Register it in RuleRegistry::with_all_rules in src/rules/registry.rs.
  3. Add fixtures/my_rule/{valid,invalid,suppressed}.js.
  4. Add a mod my_rule block to tests/integration.rs.

Suppression and extension filtering are handled by the runner, so a rule contains detection logic and nothing else. Full guide with a worked example: docs/ADDING_A_RULE.md.

Library use

use std::path::Path;
use custom_biome_lint::{lint_source, RuleRegistry};

let registry = RuleRegistry::with_all_rules();
let violations = lint_source(source, Path::new("a.js"), &registry.all());

Layout

src/
  bin/custom-biome-lint.rs   CLI entry point
  lib.rs                     library exports
  cli/                       arg parsing, help, verbosity-gated logging
  config/                    package.json ignore list
  analyzer/                  file discovery, glob matching, single-pass runner
  semantic/                  lexical scope/binding model, identifier resolution
  rules/                     Rule trait, registry, one module per rule
  suppress/                  custom-biome-ignore-line / -next-line parsing
  fixer.rs                   --write-fix: safe suppression-comment placement
  autofix.rs                 --auto-fix: applies each rule's own Fix in place
  diagnostics/               Violation/Fix types and ESLint-style formatter
fixtures/<rule_name>/        valid.js, invalid.js, suppressed.js, edge-cases.js per rule
tests/integration.rs         end-to-end rule, config and pattern tests
scripts/benchmark.sh         re-runnable cold/warm/rayon/rule-cost benchmark
scripts/set-version.js       syncs Cargo.toml + package.json + package-lock.json + npm/*/package.json to one version
bin/cli.js                   npm launcher: resolves platform, execs the precompiled binary
bin/platform.js              pure platform/arch/libc -> package name mapping, used by cli.js and its tests
npm/<platform>/package.json  one per supported platform, carries only the compiled binary
docs/                        architecture, rules, testing, setup, CI, migration, caching, benchmarking, distribution
.github/workflows/ci.yml                  build, test, fmt, clippy, audit, deny
.github/workflows/publish.yml             tag-triggered: build all 8 targets, publish all 9 npm packages
.github/workflows/biome-upgrade-check.yml monthly + on-demand: can we bump Biome yet?
rustfmt.toml                  formatting config (cargo fmt)
deny.toml                     license/advisory/source policy (cargo deny)

Portability

Self-contained: the only dependencies are Biome's parser crates and serde_json. Glob matching is implemented in analyzer/file_matcher.rs rather than pulled from a crate, and nothing reads from the surrounding repository except the package.json it discovers at runtime. The directory can be moved to its own repository, published to npm, or used as a git submodule without edits.

The Biome crates are consumed from a git pin of Biome 2.5.8 (see Cargo.toml). The standalone biome_* crates on crates.io are frozen at 0.5.7, which cannot parse $-prefixed identifiers (e.g. the Cypress $el convention), so the monorepo git source is required. Do not loosen the git pin — see docs/ARCHITECTURE.md.

Testing

cargo test                                    # 157 tests: 88 unit + 68 integration + 1 doctest
cargo fmt --all -- --check                    # no diff expected
cargo clippy --all-targets -- -D warnings     # no warnings expected
cargo audit                                   # no advisories beyond .cargo/audit.toml's ignore list
cargo deny check                              # licenses, bans, sources all ok
./target/release/custom-biome-lint fixtures   # 11 errors across 6 files
npm run test:js                               # JS launcher (bin/cli.js) tests

Full procedure, including running against the real <PRIVATE_REPO> tree and how the portability check was done: docs/TESTING.md.