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

oxlint-plugin-legibility

v0.0.5

Published

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

Readme

oxlint plugin legibility

Socket Badge 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 Oxlint JS plugin rules for readable, explicit, performance-conscious JavaScript and TypeScript.

Requires Node.js 22 or newer and Oxlint 1.55.0 or newer. Oxlint loads the package through its JavaScript plugin support.

pnpm add -D oxlint oxlint-plugin-legibility

Configs

Choose the preset for your severity and workflow in oxlint.config.ts.

legibility.configs.recommended

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

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

export default defineConfig(legibility.configs.recommended);

legibility.configs.strict

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

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

export default defineConfig(legibility.configs.strict);

legibility.configs.agentRecommended

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

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

export default defineConfig(legibility.configs.agentRecommended);

legibility.configs.agentStrict

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

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

export default defineConfig(legibility.configs.agentStrict);

All presets explicitly configure these built-in Oxlint rules:

  • 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. agentRecommended and agentStrict 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) {
-       if (user.canReceiveInvites) {
-         sendInvite(user);
-       }
-     }
-   }
- }
+ if (!user) return;
+ if (!user.active) return;
+ if (!user.email) return;
+ if (!user.canReceiveInvites) 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.locked && (user.role === "admin" || user.role === "owner");
+ const isAdmin = user?.role === "admin";
+ const isOwner = user?.role === "owner";
+ const hasPrivilegedRole = isAdmin || isOwner;
+
+ return user && user.active && !user.locked && 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, literal, or this before object construction and returns. Destructuring bindings and their defaults are excluded; computed values inside an object created by a default are still checked.

do / don't

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

With objectValues: "named":

- 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, repeated conditions, and updates. One-time loop initializers and iterable expressions are excluded. Known strings are excluded using the same checks as no-repeated-collection-search; array and unknown receivers remain checked.

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 comparisons against booleans when the other expression is provably boolean. Simplify boolean-only ternaries with !!condition or !condition to preserve their result type.

options

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

do / don't

- return (count > 0) === true;
+ return count > 0;

legibility/no-redundant-nullish-fallback()

Avoid ?? undefined fallbacks only when the left expression is provably never null. Keep value ?? undefined when it normalizes a possible null, and preserve locally bound undefined values.

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 = (void 0) ?? undefined;
+ const value = void 0;

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

Flag repeated searches over the same collection in one scope. String literals, template literals, const bindings initialized with either, and bindings annotated as string are excluded. Substring searches cannot be replaced by array membership lookups.

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. A member value named for a direct return, such as const value = item.value; return value;, is allowed so this rule agrees with no-computed-values. Identifier renames and member aliases passed to other functions remain checked.

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 getUser = fetchUser;

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 or potentially unsafe regular expressions and empty identifiers never match. Matcher lists accept at most 16 expressions, each at most 256 characters long; regex matching is limited to the first 1024 characters of a comment. 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, pnpm exec lint-changed --comments=forbid rejects every added comment, including separated comments. Outside that session policy, 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 Oxlint with --fix to apply the autofix.


legibility/no-unnecessary-async()

Flag async functions that only return one awaited value outside a try statement, or only await Node filesystem operations with synchronous equivalents. Functions without an await are unchanged because async preserves promise and error-handling semantics. 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 whose callers can accept synchronous results and errors. Non-blocking filesystem I/O remains appropriate in request-serving code. For a redundant return await, keep async and consider whether the await improves stack traces before removing it.

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 { defineConfig } from "oxlint";
 import legibility from "oxlint-plugin-legibility";

+const compositionRules = {
+  "legibility/prefer-concat-object-assign": "warn",
+} as const;
+const rules = Object.assign({}, legibility.configs.recommended.rules, compositionRules);
+
-export default defineConfig(legibility.configs.recommended);
+export default defineConfig({ ...legibility.configs.recommended, rules });

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

+#!/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:

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

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

const rules = Object.assign({}, legibility.configs.recommended.rules, filenameRules);

export default defineConfig({ ...legibility.configs.recommended, rules });
- 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:

pnpm exec 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.

Inline 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:

pnpm exec lint-changed origin/develop --comments=forbid

Keep normal comment checks

Run changed-file linting without the flag:

pnpm exec 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 { defineConfig } from "oxlint";
 import legibility from "oxlint-plugin-legibility";

+const approvedPrefixes = ["WHY:"];
+const commentOptions = { prefixIdentifiers: approvedPrefixes };
+const approvedCommentRule = ["error", commentOptions] as const;
+const commentRules = {
+  "legibility/no-unmatched-comments": approvedCommentRule,
+};
+const rules = Object.assign({}, legibility.configs.recommended.rules, commentRules);
+
-export default defineConfig(legibility.configs.recommended);
+export default defineConfig({ ...legibility.configs.recommended, rules });
- // 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:

pnpm exec lint-changed origin/main

Run the same project policy in pre-commit checks:

pnpm exec 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 Oxlint

Use a preset in oxlint.config.ts:

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

export default defineConfig(legibility.configs.recommended);

Configure rules directly:

import { defineConfig } from "oxlint";

export default defineConfig({
  jsPlugins: [{ name: "legibility", specifier: "oxlint-plugin-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("oxlint-plugin-legibility");

JSON Configuration

For .oxlintrc.json, register the plugin and configure rules explicitly:

{
  "jsPlugins": [
    {
      "name": "legibility",
      "specifier": "oxlint-plugin-legibility"
    }
  ],
  "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
      }
    ]
  }
}

Run Oxlint with the project config:

pnpm exec oxlint .

Agent Skill

Install the packaged agent skill after installing the npm package:

pnpm exec oxlint-plugin-legibility-install-skill

Install for a specific agent target:

pnpm exec oxlint-plugin-legibility-install-skill --target codex
pnpm exec oxlint-plugin-legibility-install-skill --target claude

API

Rules are configured through Oxlint rules.

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

Benchmarks

Measured on September 9, 2026 in Docker (Linux arm64, Node 26.8.1, Hyperfine 1.15.0). Each command lints 50 copies of the same JavaScript fixture, using its recommended preset and one lint worker. Times include CLI startup, parsing, rule execution, and JSON output. Each row has three warmups and 20 measured runs, with the linter cache disabled.

| Plugin | Engine | Enabled rules | Median (ms) | Mean ± σ (ms) | | --- | --- | ---: | ---: | ---: | | oxlint-plugin-legibility 0.3.5 | Oxlint 1.82.0 | 24 | 81.8 | 82.3 ± 4.6 | | eslint-plugin-legibility 0.4.0 | ESLint 10.10.0 | 24 | 147.0 | 146.9 ± 5.4 | | eslint-plugin-unicorn 74.0.0 | ESLint 10.10.0 | 308 | 522.2 | 605.7 ± 206.7 | | eslint-plugin-sonarjs 4.2.0 | ESLint 10.10.0 | 217 | 322.1 | 328.9 ± 23.4 |

Preset coverage differs, and no TypeScript type information is supplied. Both Legibility presets report 350 diagnostics; Unicorn reports 350 and SonarJS reports 50. Oxlint's default correctness category is disabled so only the preset's rules run. Unicorn had statistical outliers; raw measurements retain every sample. This small synthetic workload does not establish performance on larger projects.

nub run benchmark:e2e

The benchmark Docker target installs Hyperfine and the locked comparison dependencies during the image build, outside the timed runs. It uses Node 26 and the Oxlint version in pnpm-lock.yaml. The script checks file counts and diagnostics before timing, then prints a Markdown table and JSON results. Set BENCHMARK_ITERATIONS to change the sample count; no timing threshold is enforced.


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, plus package-loading checks in Bun and Deno; Docker package-consumer tests cover Oxlint 1.55.0 and 1.82.0.
  • 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.
  • TypeScript 7 strict checking and Oxlint's typescript/no-explicit-any rule guard the source type-safety boundary.

License

MIT