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

eslint-plugin-legibility

v0.4.0

Published

ESLint and Oxlint JS plugin rules for readable, performance-conscious code.

Readme

eslint plugin legibility

npm version npm downloads CI OpenSSF Scorecard codecov GitHub stars

Why was this written?

Working with LLMs for the majority of my work, I find the way that I code and read code has changed. This project contains rules I find useful for keeping TypeScript and/or JavaScript more readable when written mainly by LLMs.

TLDR;

The goal of rules in this package are to make code readable for reviewing lots of code and avoiding things that have a high probability of complexity or confusion.


Install

This project provides ESLint and Oxlint-compatible rules for readable, explicit, performance-conscious JavaScript and TypeScript.

The package exports an ESLint-compatible root plugin and an Oxlint-focused eslint-plugin-legibility/oxlint entry. ESLint loads the root package directly. Oxlint loads the Oxlint entry through JavaScript plugin support.

# npm, pnpm, bun
npm add -D eslint-plugin-legibility

Configs

Choose the preset for your linter and severity.

flat/recommended

Enables the broadly applicable legibility rules and core complexity limits as warnings.

import legibility from "eslint-plugin-legibility";

export default [legibility.configs["flat/recommended"]];

flat/strict

Enables every recommended rule plus the more opinionated analysis rules as errors.

import legibility from "eslint-plugin-legibility";

export default [legibility.configs["flat/strict"]];

flat/agent-recommended

Mirrors flat/recommended, but requires named values before object construction and returns.

import legibility from "eslint-plugin-legibility";

export default [legibility.configs["flat/agent-recommended"]];

flat/agent-strict

Mirrors flat/strict, but requires named values before object construction and returns.

import legibility from "eslint-plugin-legibility";

export default [legibility.configs["flat/agent-strict"]];

oxlint.configs.recommended

Mirrors flat/recommended in oxlint.config.ts.

import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";

export default defineConfig(legibility.configs.recommended);

oxlint.configs.strict

Mirrors flat/strict in oxlint.config.ts.

import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";

export default defineConfig(legibility.configs.strict);

oxlint.configs.agentRecommended

Mirrors flat/agent-recommended in oxlint.config.ts.

import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";

export default defineConfig(legibility.configs.agentRecommended);

oxlint.configs.agentStrict

Mirrors flat/agent-strict in oxlint.config.ts.

import { defineConfig } from "oxlint";
import legibility from "eslint-plugin-legibility/oxlint";

export default defineConfig(legibility.configs.agentStrict);

All presets explicitly configure these core rules because ESLint's recommended config does not enable them:

  • complexity: maximum cyclomatic complexity of 20.
  • max-lines-per-function: max: 40, excluding blank lines and comments and including IIFEs.

Rules

recommended contains broadly applicable legibility checks. strict includes every recommended rule plus more opinionated performance and code-shape analysis. agent-recommended and agent-strict keep the same rule membership as their base presets, but make computed object and return values stricter for agent-authored code. Composition style, executable-entry checks, filename schemas, and blanket comment policies remain opt-in because they require a project decision.


legibility/hoist-if-operators({options})

Prefer a named boolean before an operator-heavy if condition.

options

  • {max: number}: allowed weighted operators in the condition. Default: 0.
  • {operators: string[]}: operators to count. Default: ["&&", "||", "??", "?:"].
  • {complexity: Record<string, number>}: per-operator weights.

do / don't

- if (user && user.isActive && !user.isLocked) {
-   sendInvite(user);
- }
+ const canInviteUser = user && user.isActive && !user.isLocked;
+
+ if (canInviteUser) {
+   sendInvite(user);
+ }

legibility/max-array-chain-depth({options})

Limit chained array methods like items.filter().map().some().

options

  • {max: number}: allowed chained items. Default: 2.
  • {iterationMethods: string[]}: method names that count as chain items.

do / don't

- const hasLargeActiveItem = items
-   .filter((item) => item.active)
-   .map((item) => item.size)
-   .some((size) => size > 100);
+ const activeItems = items.filter((item) => item.active);
+ const itemSizes = activeItems.map((item) => item.size);
+ const hasLargeActiveItem = itemSizes.some((size) => size > 100);

legibility/max-control-flow-depth({options})

Limit nested branches and loops.

options

  • {max: number}: allowed nested control-flow depth. Default: 3.

do / don't

- if (user) {
-   if (user.active) {
-     if (user.email) {
-       sendInvite(user);
-     }
-   }
- }
+ if (!user) return;
+ if (!user.active) return;
+ if (!user.email) return;
+
+ sendInvite(user);

legibility/max-expression-operators({options})

Limit operators inside one expression.

options

  • {max: number}: allowed weighted operators. Default: 4.
  • {operators: string[]}: operators to count.
  • {complexity: Record<string, number>}: per-operator weights.

do / don't

- return user && user.active && (user.role === "admin" || user.role === "owner");
+ const isAdmin = user.role === "admin";
+ const isOwner = user.role === "owner";
+ const hasPrivilegedRole = isAdmin || isOwner;
+
+ return user && user.active && hasPrivilegedRole;

legibility/max-function-parameters({options})

Limit the inputs a function exposes. The rule checks both top-level parameters and the properties listed by each destructured object parameter.

TypeScript ambient declarations and function types are checked. A leading TypeScript this parameter is ignored because callers do not supply it.

options

  • {max: number}: allowed top-level parameters. Default: 4.
  • {maxObjectProperties: number}: allowed properties in one destructured object parameter. Default: 8.

do / don't

- function schedule(user, plan, timezone, locale, notify) {}
+ function schedule(request, deliveryOptions) {}

- function publish({ article, author, channel, locale, schedule, tags, theme, tracking, visibility }) {}
+ function publish(article, publicationOptions) {}

legibility/no-complex-ternaries({options})

Reject nested ternaries and operator-heavy ternaries.

options

  • {max: number}: allowed weighted operators inside one ternary. Default: 2.
  • {operators: string[]}: operators to count.
  • {complexity: Record<string, number>}: per-operator weights.

do / don't

- const label = isLoading ? "Loading" : hasError ? "Error" : "Ready";
+ const label = getStatusLabel({ hasError, isLoading });

legibility/no-computed-values({options})

Prefer named values before computed returns and object values.

options

  • {max: number}: allowed weighted operators in a computed value. Default: 1.
  • {operators: string[]}: operators to count.
  • {complexity: Record<string, number>}: per-operator weights.
  • {objectValues: "computed" | "named"}: object value mode. Default: "computed".
  • {returnValues: "computed" | "named"}: return value mode. Default: "computed".

Use "named" mode to require a named identifier or literal before object construction and returns.

do / don't

- return subtotal + tax - discount;
+ const total = subtotal + tax - discount;
+
+ return total;

- return { route: getRouteName(url.pathname) };
+ const route = getRouteName(url.pathname);
+
+ return route;

legibility/no-direct-node-bin-smoke({options})

Smoke-test installed package bins instead of direct node src/index.js execution.

options

  • {entryPatterns: string[]}: entry files that should be tested through the installed bin shim.

do / don't

- execSync("node src/index.js --help");
+ execSync("my-cli --help");

legibility/no-hidden-side-effects({options})

Keep mutations out of nested expressions and side-effect-free callbacks.

options

  • {mutatingMethods: string[]}: method calls treated as mutations.
  • {sideEffectFreeIterationMethods: string[]}: callback methods expected to stay side-effect-free.

do / don't

- return (count += 1);
+ count += 1;
+
+ return count;

legibility/no-identity-array-callback()

Reject map and filter callbacks that keep every item unchanged.

do / don't

- const nextItems = items.map((item) => item);
+ const nextItems = items;

legibility/no-mixed-filename-casing()

Use one filename convention: kebab-case, camelCase, PascalCase, or snake_case. Leading dots and file extensions are ignored.

This rule has no options. It rejects conventions mixed within one filename; it does not require every file in the project to use the same convention. For example, user-profile.ts, userProfile.ts, UserProfile.ts, and user_profile.ts are all valid.

do / don't

- my-File.ts
+ my-file.ts

- user_profile-card.test.ts
+ user_profile_card.test.ts

- accountSettings-helper.ts
+ account-settings-helper.ts

legibility/no-quadratic-patterns({options})

Flag nested loops, nested array iteration, and collection searches inside loop bodies.

options

  • {iterationMethods: string[]}: methods checked for nested iteration.
  • {searchMethods: string[]}: methods treated as collection searches.

do / don't

- const enrichedOrders = orders.map((order) => ({
-   ...order,
-   user: users.find((user) => user.id === order.userId),
- }));
+ const usersById = new Map(users.map((user) => [user.id, user]));
+ const enrichedOrders = orders.map((order) => ({
+   ...order,
+   user: usersById.get(order.userId),
+ }));

legibility/no-redundant-boolean-logic({options})

Avoid boolean comparisons and boolean-only ternaries.

options

  • {equalityOperators: string[]}: operators checked for comparisons against true or false. Default: ["==", "===", "!=", "!=="].

do / don't

- return isReady === true ? true : false;
+ return isReady;

legibility/no-redundant-nullish-fallback()

Avoid ?? undefined fallbacks.

Static void operands are evaluated within a bounded BigInt budget. Expressions that could create unusually large BigInt values are ignored.

do / don't

- const value = maybeValue ?? undefined;
+ const value = maybeValue;

legibility/no-repeated-collection-search({options})

Flag repeated searches over the same collection in one scope.

options

  • {searchMethods: string[]}: methods treated as collection searches.

do / don't

- const owner = users.find((user) => user.id === ownerId);
- const reviewer = users.find((user) => user.id === reviewerId);
+ const usersById = new Map(users.map((user) => [user.id, user]));
+ const owner = usersById.get(ownerId);
+ const reviewer = usersById.get(reviewerId);

legibility/no-small-collection-conversion({options})

Avoid converting a statically small array or string into a Map or Set for one immediate lookup. Named collections, dynamic inputs, and literal inputs at the threshold are unchanged.

options

  • {min: number}: minimum known input size before a lookup collection is useful. Default: 3.

do / don't

- const isTerminal = new Set(["done", "failed"]).has(status);
+ const isTerminal = ["done", "failed"].includes(status);

legibility/no-single-use-renaming-alias()

Avoid aliases that only rename another value for one use.

do / don't

- const userData = user;
-
- return userData.name;
+ return user.name;

legibility/no-standalone-array-mutations({options})

Prefer explicit returned array composition over standalone array mutation statements.

options

  • {arrayMutatingMethods: string[]}: array methods reported when used as standalone mutations.
  • {mutatingMethods: string[]}: mutation methods used to identify fresh mutation targets.

do / don't

- items.push(nextItem);
-
- return items;
+ return items.concat(nextItem);

legibility/no-trivial-wrapper-functions()

Avoid wrappers that only forward their parameters to another call.

do / don't

- const getUser = (userId) => fetchUser(userId);
+ const getActiveUser = (userId) => fetchUser(userId).then(requireActiveUser);

legibility/no-unmatched-comments({options})

Allow only comments that match an explicit pattern, prefix, or suffix. With no options, the rule rejects every line and block comment. Executable shebangs are ignored.

options

  • matchers: case-insensitive regular expressions matched against the comment body.
  • prefixIdentifiers: case-insensitive identifiers allowed at the start of a comment.
  • suffixIdentifiers: case-insensitive identifiers allowed at the end of a comment.

All options accept string arrays and default to empty arrays.

The three options are independent allow paths. Matching ignores comment delimiters, surrounding whitespace, and leading JSDoc stars. Invalid regular expressions and empty identifiers never match. The rule has no autofix.

do / don't

With the WHY: prefix configured:

- // Retry after the provider resets its rate limit.
+ // WHY: The provider resets its rate limit every 30 seconds.

legibility/no-stacked-comments()

Reject comments on consecutive lines. A blank line between comments is allowed.

do / don't

- // Retry every failed request.
- // Retry requests that fail during regional failover.
+ // Retry only requests that fail during regional failover.

The rule has no options and no autofix.


legibility/no-automated-comment-attribution({options})

Reject comments that explicitly attribute authorship to an automated tool. The rule detects configured identifiers in attribution tags, generated by <identifier>, and <identifier>-generated. It does not classify unmarked prose.

options

  • {identifiers: string[]}: case-insensitive names treated as automated sources. Default: ai, chatgpt, claude, codex, copilot, gemini, gpt, llm, and openai.

do / don't

- // Generated by Codex. Normalize provider errors before retrying.
+ // Normalize provider errors before retrying.

legibility/require-jsdoc-multiline-comments()

Require block comments spanning multiple lines to use /** ... */ JSDoc syntax. Line comments and single-line block comments are unchanged.

This formatting rule does not permit agents to add comments. During an agent session, npx lint-changed --comments=forbid rejects every added comment, including separated comments. Outside that session policy, ESLint and Oxlint can autofix this rule by adding the missing * to the block opener.

do / don't

- /*
-  * The provider can return a stale token during regional failover.
-  * Preserve the retry order.
-  */
+ /**
+  * The provider can return a stale token during regional failover.
+  * Preserve the retry order.
+  */

This rule has no options. Run ESLint or Oxlint with --fix to apply the autofix.


legibility/no-unnecessary-async()

Flag async functions that have no await, only return one awaited value, or only await Node filesystem operations with synchronous equivalents. Filesystem detection covers named and namespace imports from node:fs/promises, fs/promises, node:fs, and fs.

Use the filesystem diagnostic for local tooling and scripts. Non-blocking filesystem I/O remains appropriate in request-serving code.

do / don't

- import { readFile } from "node:fs/promises";
+ import { readFileSync } from "node:fs";

- async function readConfig() {
-   return await readFile("config.json", "utf8");
+ function readConfig() {
+   return readFileSync("config.json", "utf8");
  }

legibility/no-unnecessary-block-callback()

Prefer expression-bodied arrow callbacks when the callback block only returns.

do / don't

- const ids = users.map((user) => {
-   return user.id;
- });
+ const ids = users.map((user) => user.id);

legibility/prefer-concat-object-assign()

Report array and object literals containing spread when a project prefers method-based composition:

  • Array literal spread is reported in favor of Array#concat.
  • Object literal spread is reported in favor of Object.assign with a new target.
  • Function-call spread and rest syntax are unchanged.

This rule has no options or autofix. Enable it explicitly:

 import legibility from "eslint-plugin-legibility";

+const compositionRules = {
+  "legibility/prefer-concat-object-assign": "warn",
+};
+const compositionConfig = { rules: compositionRules };
+
 export default [
   legibility.configs["flat/recommended"],
+  compositionConfig,
 ];

why it is opt-in

This is a style opinion, not a universal performance rule. concat names the array composition operation. Object.assign names the object composition operation, makes the fresh target visible, and preserves source precedence in argument order.

ESLint's opposing prefer-object-spread rule says object spread may perform better. V8's spread documentation describes a fast path when spread begins an array literal, including [...items, nextItem], but not when values precede it, as in [firstItem, ...items]. Engine, placement, collection size, and data shape can change the result, so neither form is always faster.

The forms can also behave differently. Object.assign uses assignment semantics, while object spread creates data properties. concat observes concat-spreadability, while array spread uses iteration. The rule therefore reports the syntax but leaves the change to the developer.

do / don't

For ordinary dense arrays and plain objects where the behavior is equivalent:

- const nextItems = [...items, ...moreItems];
- const appendedItems = [...items, nextItem];
- const prependedItems = [firstItem, ...items];
- const options = { ...defaults, enabled: true };
+ const nextItems = items.concat(moreItems);
+ const appendedItems = items.concat([nextItem]);
+ const prependedItems = [firstItem].concat(items);
+ const options = Object.assign({}, defaults, { enabled: true });

Wrapping nextItem in an array prevents concat from flattening it when the value is itself an array. The rule reports each containing literal once and does not autofix because custom iterators, concat-spreadability, sparse arrays, setters, and proxies can change behavior. Review each diagnostic for equivalent behavior.


legibility/prefer-early-return()

Avoid else branches after an if branch already exits.

do / don't

- if (!user) {
-   return null;
- } else {
-   return user.name;
- }
+ if (!user) {
+   return null;
+ }
+
+ return user.name;

legibility/prefer-flat-map()

Prefer flatMap over map(...).flat().

do / don't

- const permissions = users.map((user) => user.permissions).flat();
+ const permissions = users.flatMap((user) => user.permissions);

legibility/prefer-guard-clauses()

Prefer guard clauses over wrapping a whole function body in one branch.

do / don't

- function sendInvite(user) {
-   if (user) {
-     const email = buildEmail(user);
-     deliver(email);
-   }
- }
+ function sendInvite(user) {
+   if (!user) return;
+
+   const email = buildEmail(user);
+   deliver(email);
+ }

legibility/prefer-object-lookup({options})

Prefer Set, Map, or object lookups over long equality || chains.

options

  • {min: number}: equality checks required before reporting. Default: 3.
  • {operators: string[]}: equality operators that count. Default: ["==", "==="].

do / don't

- const isSupported = type === "page" || type === "post" || type === "asset";
+ const supportedTypes = new Set(["page", "post", "asset"]);
+ const isSupported = supportedTypes.has(type);

legibility/prefer-positive-condition-names({options})

Prefer positive boolean names over names like isNotReady.

options

  • {booleanOperators: string[]}: binary operators that mark an initializer as boolean-like.

do / don't

- const isNotReady = status !== "ready";
-
- if (!isNotReady) {
-   run();
- }
+ const isReady = status === "ready";
+
+ if (isReady) {
+   run();
+ }

legibility/require-executable-shebang({options})

Require configured CLI entry source files to include a Node, Bun, or Deno shebang.

This rule is opt-in because a common source index is not necessarily executable. Enable it only for actual command entry paths.

options

  • {files: string[]}: source files expected to be executable entries.
  • {runtimes: string[]}: accepted shebang runtimes. Default: ["bun", "deno", "node"].

do / don't

- console.log("hello");
+#!/usr/bin/env node
+
+ console.log("hello");

legibility/require-filename-matches-dirname({options})

Require filenames to match an explicitly selected schema. The rule is not included in a preset because projects must choose dirname, index, or a custom schema.

options

  • {schema: "dirname" | "index" | "custom"}: required filename schema.
  • {minDepth: number}: minimum parent depth to check. Default: 3.
  • {allowedQualifiers: string[]}: dirname schema suffixes.
  • {allowedFilenames: string[]}: dirname schema standalone basenames.
  • {patterns: string[]}: required exact basenames for a custom schema. Use {dirname} as the parent-directory placeholder.

The rule ignores the final JavaScript or TypeScript extension, so the same schema covers .js, .jsx, .ts, and .tsx files.

index schema

The index schema allows only constants, index, index.test, types, utils, and utils.test:

const filenameSchema = { schema: "index", minDepth: 3 };
const filenameRules = {
  "legibility/require-filename-matches-dirname": ["error", filenameSchema],
};

export default [
  legibility.configs["flat/recommended"],
  { rules: filenameRules },
];
- src/components/button/button.ts
- src/components/button/button.test.ts
+ src/components/button/index.ts
+ src/components/button/index.test.tsx
+ src/components/button/utils.ts

dirname schema

The dirname schema keeps the existing directory-name convention. It allows button, qualified forms such as button.test, and standalone filenames such as index under src/components/button/:

const filenameSchema = { schema: "dirname", minDepth: 3 };
const filenameRules = {
  "legibility/require-filename-matches-dirname": ["error", filenameSchema],
};
- src/components/button/useButton.ts
- src/components/button/button.effect.ts
+ src/components/button/button.ts
+ src/components/button/button.test.ts
+ src/components/button/index.ts

The default qualifiers are constants, helpers, spec, styles, test, types, and utils. The default standalone filenames are constants, index, types, and utils. The option arrays replace those defaults.

custom schema

Custom patterns match the basename exactly after replacing {dirname}:

const filenameSchema = {
  schema: "custom",
  minDepth: 3,
  patterns: ["{dirname}", "{dirname}.test", "index", "index.test", "schema"],
};
const filenameRules = {
  "legibility/require-filename-matches-dirname": ["error", filenameSchema],
};

Operator Options

The operator-counting rules accept the same option shape:

  • legibility/hoist-if-operators
  • legibility/max-expression-operators
  • legibility/no-complex-ternaries
  • legibility/no-computed-values
{
  rules: {
    "legibility/max-expression-operators": [
      "warn",
      {
        max: 4,
        operators: ["&&", "||", "??", "?:", "!", "===", "!=="],
        complexity: { "?:": 2 }
      }
    ]
  }
}

Chain And Count Options

Use max and min to tune rule sensitivity.

{
  rules: {
    "legibility/max-array-chain-depth": ["warn", { max: 3 }],
    "legibility/max-control-flow-depth": ["warn", { max: 2 }],
    "legibility/prefer-object-lookup": ["warn", { min: 4 }]
  }
}

Recipes

The bundled presets check comment quality. They do not ban every comment. Use a session flag or configure no-unmatched-comments when comments need an explicit allow policy.

Block comments during an agent session

Pass --comments=forbid to the changed-file lint command:

npx lint-changed --comments=forbid

The flag enables legibility/no-unmatched-comments as an error for that invocation. It does not change the project config. Every comment in a new file fails. In modified files, only comments that intersect added lines fail, so existing comments outside the session diff remain untouched.

ESLint disable directives cannot suppress the session policy. Parse or configuration failures fail the command, and a pure file rename does not turn existing comments into additions.

Pass the base branch before or after the flag:

npx lint-changed origin/develop --comments=forbid

Keep normal comment checks

Run changed-file linting without the flag:

npx lint-changed

This uses the project config. The bundled presets still reject automated attribution, stacked comments, and non-JSDoc multiline blocks.

Allow only marked comments

Configure no-unmatched-comments directly when a repository permits a small set of durable comments:

 import legibility from "eslint-plugin-legibility";

+const approvedPrefixes = ["WHY:"];
+const commentOptions = { prefixIdentifiers: approvedPrefixes };
+const approvedCommentRule = ["error", commentOptions];
+const commentRules = {
+  "legibility/no-unmatched-comments": approvedCommentRule,
+};
+const commentConfig = { rules: commentRules };
+
 export default [
   legibility.configs["flat/recommended"],
+  commentConfig,
 ];
- // Wait before retrying.
+ // WHY: The provider resets its rate-limit window every 30 seconds.
  const retryDelayMs = 30_000;

Keep CI and pre-commit on repository policy

Omit the session flag from commit and pull-request gates:

npx lint-changed origin/main

Run the same project policy in pre-commit checks:

npx lint-changed

These checks allow comments unless the project config explicitly restricts them. The bundled comment-quality rules still apply. Reserve --comments=forbid for active agent sessions.


Usage

Using With ESLint

Flat config:

import legibility from "eslint-plugin-legibility";

export default [legibility.configs["flat/recommended"]];

Configure rules directly:

import legibility from "eslint-plugin-legibility";

export default [
  {
    plugins: { legibility },
    rules: {
      "legibility/max-array-chain-depth": ["warn", { max: 2 }],
      "legibility/max-expression-operators": ["warn", { max: 4 }],
      "legibility/no-quadratic-patterns": "warn",
    },
  },
];

CommonJS compatibility:

const legibility = require("eslint-plugin-legibility");

Usage With Oxlint

Oxlint JavaScript plugins use the same ESLint-compatible rule API. Use a legibility.configs.* preset from eslint-plugin-legibility/oxlint with oxlint.config.ts. For .oxlintrc.json, register the plugin and configure rules explicitly:

{
  "jsPlugins": [
    {
      "name": "legibility",
      "specifier": "eslint-plugin-legibility/oxlint"
    }
  ],
  "rules": {
    "legibility/max-array-chain-depth": ["warn", { "max": 2 }],
    "legibility/max-expression-operators": ["warn", { "max": 4 }],
    "legibility/no-quadratic-patterns": "warn",
    "complexity": ["warn", 20],
    "max-lines-per-function": [
      "warn",
      {
        "max": 40,
        "skipBlankLines": true,
        "skipComments": true,
        "IIFEs": true
      }
    ]
  }
}

Agent Skill

Install the packaged agent skill after installing the npm package:

npx eslint-plugin-legibility-install-skill

Install for a specific agent target:

npx eslint-plugin-legibility-install-skill --target codex
npx eslint-plugin-legibility-install-skill --target claude

API

Rules are configured through ESLint or Oxlint rules.

{
  "rules": {
    "legibility/rule-name": ["warn", { "option": "value" }]
  }
}

Security Posture

  • No runtime dependencies.
  • Published package contents are allowlisted with files.
  • Releases are tag-triggered and publish GitHub release assets.
  • npm publishing uses GitHub Actions trusted publishing with provenance.
  • CI runs tests on Node 22, 24, and 26; Docker package-consumer tests cover supported ESLint and Oxlint ranges; compatibility suites run on Bun and Deno.
  • Codependence maintains pnpm dependencies, GitHub Actions, and Docker image pins.
  • Pastoralist audits CVE overrides in pnpm-workspace.yaml and records their metadata in package.json.

Development

The repository uses Mise for Node 26 and Nub for pnpm 11. Nub keeps pnpm-lock.yaml as the package-manager source of truth.

nub install --frozen-lockfile
nub run validate

Docker end-to-end tests

Build the package tarball, install it in an isolated consumer, and test ESLint and Oxlint with the default, opt-in, recommended, and strict fixture profiles:

nub run test:e2e

Benchmark every engine and fixture setup against one file and a generated 100-file project:

nub run benchmark:e2e

The local image defaults to Node 26, ESLint 9, and Oxlint 1.78. Set E2E_NODE_VERSION, E2E_ESLINT_VERSION, and E2E_OXLINT_VERSION to test another supported combination. CI covers the oldest supported ESLint and Oxlint releases and the current major releases across Node 22, 24, and 26.

The benchmark reports JSON with mean, median, p95, minimum, maximum, and mean-per-file duration. Adjust its sample counts with BENCHMARK_WARMUPS and BENCHMARK_ITERATIONS. Benchmarks report measurements without enforcing timing thresholds. Both commands remove their Compose containers, networks, volumes, and local e2e image after success or failure.


License

MIT