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

@aurelienbbn/oxlint-plugin-core

v0.3.0

Published

Custom oxlint rules for TypeScript projects.

Readme

@aurelienbbn/oxlint-plugin-core

npm downloads CI license node

17 oxlint rules for any TypeScript codebase: swallowed errors, flaky or hollow tests, test code in production, anonymous public contracts, cramped statements.

pnpm add -D @aurelienbbn/oxlint-plugin-core oxlint   # oxlint >=1.82.0 <2.0.0
{
  "jsPlugins": ["@aurelienbbn/oxlint-plugin-core"],
  "rules": {
    "core/no-discarded-caught-error": "error",
    "core/no-weak-test-assertions": ["error", { "assertionHelpers": ["assertValid"] }],
    "core/no-test-sleeps": "error"
  }
}

No preset, one kind of autofix. Enable each rule by name; every fix but the padding rules' blank lines needs a decision a tool can't make.

| Job | Rules | | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🧯 errors | no-discarded-caught-error, no-error-message-matching | | 🧪 tests | no-weak-test-assertions, no-test-sleeps, no-stubbed-subject, no-ambient-nondeterminism-in-tests, no-vitest-mocking, no-vitest-in-source, no-test-logic-in-production | | 📜 contracts | no-exported-anonymous-object-return, no-multi-positional-parameters, no-mutable-exported-state, no-reexport-only-modules, no-let | | 💬 comments | no-dead-comments | | ↕️ layout | padding-line-between-statements (autofix), padding-before-exit (autofix, deprecated) |

| Rule | Option | Default | | ------------------------------------ | -------------------------------------------- | --------------------------------------- | | no-discarded-caught-error | logOnlyCallees | ["console", "logger", "log"] | | | reasonMarker | "REASON" | | | includeTestFiles | false (test files exempt) | | no-ambient-nondeterminism-in-tests | allowClock | false; true waives clock reads only | | no-weak-test-assertions | assertionHelpers | [] | | no-vitest-mocking | forbidSpies | false (vi.fn, vi.spyOn allowed) | | no-reexport-only-modules | allow | ["index.ts"] | | no-multi-positional-parameters | exemptFunctionNames, exemptFileBasenames | [], [] | | padding-line-between-statements | ESLint's { blankLine, prev, next } list | the configuration shown under ↕️ below |

| Rule | The decision its fix needs | | ------------------------------------- | ----------------------------------------------------------------------- | | no-ambient-nondeterminism-in-tests | the scenario's locale, frozen instant, or seed | | no-dead-comments | a bare TODO: track it or delete it | | no-discarded-caught-error | rethrow, branch on the error, or record a reason | | no-error-message-matching | the stable discriminator (class, tag, code) depends on the error source | | no-exported-anonymous-object-return | the schema/type name and where it's exported | | no-let | restructure the value construction, not swap a keyword | | no-multi-positional-parameters | update callers, pick property names | | no-mutable-exported-state | a factory, an immutable contract, or a dependency boundary | | no-reexport-only-modules | reroute every consumer to the owning module | | no-stubbed-subject | which collaborator to replace instead | | no-test-logic-in-production | an injected dependency or a public contract | | no-test-sleeps | a condition to poll or a fake-timer schedule | | no-vitest-in-source | the right test utility or dependency seam | | no-vitest-mocking | the dependency boundary and the fake | | no-weak-test-assertions | which observable outcome each test asserts |

🧯 A caught error is handled or explained

try {
  await sync();
} catch (error) {
  console.error(error);
  return [];
} // ❌ only logged
promise.catch(() => null); // ❌ discarded
promise.catch(handleFailure); // ✅ handed to a function
try {
  access(path);
  return true;
} catch {
  // REASON: a missing file is the negative answer here
  return false; // ✅ reasoned
}

return err.message === "Not found"; // ❌ no-error-message-matching
return error instanceof TimeoutError; // ✅ class, tag, or code

no-discarded-caught-error covers catch clauses, inline .catch(fn), and an inline second argument of .then, when the handler never throws (nor returns Promise.reject(...)) and the binding is absent or only reaches console.* / logger.* / log.*.

try {
  return await load(id);
} catch (error) {
  throw new LoadError({ id, cause: error });
} // ✅ rethrown
try {
  return await load();
} catch (error) {
  if (isNotFound(error)) return undefined;
  throw error;
} // ✅ inspected

| Case | Behavior | | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | counts as handling | passing the error to any non-log function, returning it, testing it | | escape hatch | REASON: <≥ 3 words> inside the handler; promise callbacks also directly above the statement. Placeholders (todo, ignored, this is fine…) don't count | | catch {} (literally empty) | ⏭️ left to eslint/no-empty | | Effect.catch(...) | ⏭️ capitalized receivers are namespaces, skipped | | schema builders with instance .catch(fn) | ⚠️ need a reason or a disable comment |

no-error-message-matching: .message compared with a string literal, fed to includes / startsWith / endsWith / match / search / indexOf / RegExp#test (also after toLowerCase()-style calls), or used as a switch discriminant. String(error), error.toString(), `${error}` on a caught binding count too. Test files exempt. Error-shaped = catch binding, rejection-handler parameter, or a name like e, err, error, cause, reason, ex, exception, *Error, *Err, *Exception.

🧪 A test proves behavior, deterministically

no-weak-test-assertions judges per test: one strong assertion clears it.

| ❌ Weak | ✅ Strong | | ------------------------------------------ | ---------------------------------------------- | | expect(load()).toBeDefined() | expect(load()).toEqual({ id: 1 }) | | expect(() => run()).not.toThrow() | expect(() => run()).toThrow("boom") | | toHaveBeenCalledWith(expect.any(Object)) | toHaveBeenCalledWith(expect.any(String), 30) |

await new Promise((r) => setTimeout(r, 500)); // ❌ no-test-sleeps
await vi.waitFor(() => expect(state()).toBe("done")); // ✅ poll the condition

// cart.test.ts
vi.spyOn(cart, "total").mockReturnValue(30); // ❌ no-stubbed-subject
vi.spyOn(pricing, "rate").mockReturnValue(2); // ✅ another module

export const send = (mail) => (process.env.NODE_ENV === "test" ? undefined : deliver(mail)); // ❌ no-test-logic-in-production

Projects using Vitest in-source testing turn no-test-logic-in-production off for those files.

Weak assertions

| ❌ Weak | ✅ Strong | | ---------------------------------------------------- | --------------------------------------------------- | | .toBeTruthy() | an exact value | | expect(true).toBe(true) (literal vs literal) | expect(3).toBe(add(1, 2)) (literal vs computed) | | expect(result).toEqual(result) (value vs itself) | expect(render()).toMatchSnapshot() | | expect(fn()).toBe(5) after fn.mockReturnValue(5) | fc.assert(fc.property(…)) | | toEqual(expect.anything()) | toEqual({ id: expect.any(String), total: 30 }) | | bare toHaveBeenCalled() / toBeCalled() | .not.toHaveBeenCalled() ("never called" is exact) | | toBeInstanceOf(Object) | toEqual(expect.any(Order)) | | expect(typeof x).toBe("string") | |

Wildcards = expect.anything(), expect.any(Object | Function | Array | String | Number | Boolean), expect.objectContaining({}), expect.arrayContaining([]). Bare toThrow() isn't counted: vitest/require-to-throw-message owns it, pinned for test files by @aurelienbbn/oxlint-config.

Sleeps (*.test.* / *.spec.*)

await new Promise((r) => setTimeout(r, 500)); // ❌ timer-only executor
import { setTimeout as sleep } from "node:timers/promises"; // ❌ also `scheduler`
await sleep(250); // ❌ sleep / delay / wait / pause + numeric literal

Files calling vi.useFakeTimers() skip the first and last shape: their timers aren't wall-clock.

Ambient nondeterminism (concept: "Eradicating Non-Determinism in Tests", Martin Fowler)

| ❌ Fires on | Unless | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | toLocaleString / toLocaleDateString / toLocaleTimeString without a locale | — | | localeCompare without its 2nd argument | — | | Intl.*Format constructed without a locale | — | | Math.random() | — | | new Date() (no arg), Date.now(), performance.now() | file calls vi.useFakeTimers / vi.setSystemTime, or allowClock: true |

Shadowed Date / Math / Intl stay silent.

Stubbed subject (<stem>.test.* / <stem>.spec.*)

import * as cart from "./cart"; // also ./cart.js, ../cart/index
vi.spyOn(cart, "total"); // ✅ pass-through spy

Namespace or default import of the same-stem relative module; canned behavior via mockReturnValue, mockResolvedValue, mockRejectedValue, mockImplementation (and …Once), chained or on the spy's const. Package imports stay silent.

Test logic in production (concept: "Test Logic in Production", G. Meszaros, xUnit Test Patterns). Silent in test files, Vitest config and setup files, /test-utils/, /testing/ (the paths no-vitest-in-source allows).

export const send = (mail) => (process.env.NODE_ENV === "test" ? undefined : deliver(mail)); // ❌ also import.meta.env.MODE === "test"
export const retries = process.env.VITEST ? 0 : 3; // ❌ also VITEST_WORKER_ID, JEST_WORKER_ID, import.meta.vitest
export const __testing__ = { parse }; // ❌ name marks it test-only
/** Visible only for unit tests. */
export const parse = (input) => input; // ❌ comment marks it test-only

Test-only names: __test__ / __tests__ / __testing__, _internal(s), internals, testOnly*, *ForTest / *ForTests / *ForTesting. Comments like "exported for testing" or "visible only for tests" directly above the export.

Vitest APIs: no-vitest-in-source reports importing vitest from non-test source; no-vitest-mocking reports module replacement via Vitest mock APIs, plus vi.fn / vi.spyOn with forbidSpies: true.

📜 Public contracts are named

export const getUser = () => ({ id: user.id }); // ❌ no-exported-anonymous-object-return
export const load: Load = () => ({ id: user.id }); // ✅ named contract

const loadUser = (userId: UserId, includePosts: boolean) => userId; // ❌ no-multi-positional-parameters
export function loadUser(input: { userId: UserId; includePosts: boolean }) {} // ✅ object input

export let currentUserId = undefined; // ❌ no-mutable-exported-state
export * from "./user.js"; // (whole file)                    // ❌ no-reexport-only-modules, unless listed in `allow`
let total = 0; // ❌ no-let (also `var`)

💬 Comments describe code, not its history

// Updated to use the new pricing API    ❌ change narration (line comments)
// TODO: rename this contract            ❌ untracked
// TODO(#123): rename this contract      ✅ tracked (also CART-42 or a link)
/** Updated when the cache expires */    ✅ docblock describing behavior

| ❌ Fires | ✅ Stays silent | | -------------------------------------------------------------------------------------------------------- | ---------------------------------------------- | | // NEW: / // Updated: / // Fixed: labels | // New customers receive a welcome discount | | // Previously this… / // Formerly: / // Was: | // Previously seen cursors are skipped | | // Added … as requested / … for the review | // TODO CART-42: tighten the types or a link | | line comments only: // Updated to… / // Changed from… / // Now returns… / // No longer supports… | |

Also closing-brace labels and placeholder scaffolding; narration counts only when anchored at the comment start.

↕️ Blocks, declaration runs and exits stand apart

padding-line-between-statements takes ESLint's options (a list of { blankLine, prev, next }, the last match wins) and its statement types. A comment block above a statement belongs to it: the blank line goes above the comments, and a comment on the line a statement ends on stays with that statement. Autofix inserts or removes the blank lines.

function start(runtime) {
  const config = load();
  // Planning runs need no config.
  if (runtime.planning) {
    return;
  }
  runtime.apply(config); // ❌ twice: above the comment, and below the block
}

function start(runtime) {
  const config = load();

  // Planning runs need no config.
  if (runtime.planning) {
    return;
  }

  runtime.apply(config); // ✅
}

Enabled without options, it applies this configuration; any options replace it whole:

{
  "core/padding-line-between-statements": [
    "error",
    { "blankLine": "always", "prev": "*", "next": ["block-like", "multiline-expression"] },
    { "blankLine": "always", "prev": ["block-like", "multiline-expression"], "next": "*" },
    { "blankLine": "always", "prev": ["const", "let"], "next": "*" },
    { "blankLine": "any", "prev": ["const", "let"], "next": ["const", "let"] },
    { "blankLine": "always", "prev": "*", "next": ["return", "throw"] }
  ]
}

| Overlap | Status | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | padding-before-exit | ⚠️ deprecated: the default configuration covers it, and { "blankLine": "always", "prev": "*", "next": ["return", "throw"] } alone reproduces it. Turn it off when you enable padding-line-between-statements. | | effect/padding-after-dependencies | ✅ complementary: it also separates yield* service dependencies from a const of logic below them, which a declaration run leaves free. Both rules insert the same single blank line. |

⚠️ Migration

| Now reports | Example | Fix (no autofix) | | -------------- | ---------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- | | aliased export | const read = () => ({ id: 1 }); export { read as load }; | named return type, same as a direct export | | aliased Vitest | spec.each(rows)("exists", row => check(row).toBeDefined()) when spec / check alias test / expect | assert the observable value, or register a real assertion helper |

| Topic | Contract | | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | architectural policy | named exported object contracts and object-style parameters are policy, not proof of a bug | | public-return analysis | ✅ direct exports, same-file export lists/aliases, const forwarding aliases, default identifiers, explicit overload signatures · ⏭️ nested helper returns stay private · ❌ external re-exports and package entrypoint traversal not resolved | | callback arity | externally imposed callback arities exempt | | no-let | optional: deliberate local mutation can be clearer and faster | | mutable exports | forward exports handled without conflating local declarations | | weak assertions | judged per test; recognizes real expect chains and meaningful snapshot/property assertions; arbitrary matcher-named methods aren't proof | | Vitest recognition | named aliases, namespace imports, unshadowed globals, each, conditional modifiers. Local shadows stay outside the Vitest rules. Fast-check assertions support imported aliases | | assertionHelpers | trusts calls with that local name: the helper's behavioral contract is on you. Cross-module wrapper inference unsupported |

Registered contract inventory

Generated from package exports by pnpm catalog. Rule-specific options and limitations are described above and in the source tests.

| Rule/check | Trigger or review scope | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | no-ambient-nondeterminism-in-tests | Disallow test files from depending on the host locale, the real clock, or Math.random(): locale-sensitive formatting without a locale, new Date() / Date.now() / performance.now() without fake timers, and unseeded randomness. | | no-dead-comments | Disallow dead comments: closing-brace labels, placeholder scaffolding, untracked TODOs, and comments narrating a change instead of the code. | | no-discarded-caught-error | Require a catch clause or inline promise rejection handler that never rethrows and ignores or only logs the error to carry a reasoned marker comment. | | no-error-message-matching | Disallow branching on the text of an error message through string comparison, substring or regex matching, or a switch. | | no-exported-anonymous-object-return | Require named object return contracts for functions exported directly, by local alias, or as a default identifier. | | no-let | Disallow let and var declarations in favor of const and expression-oriented code. | | no-multi-positional-parameters | Require object inputs instead of multiple positional parameters for functions. | | no-mutable-exported-state | Disallow exporting mutable module state. | | no-reexport-only-modules | Disallow modules that only re-export other modules, except allowed entrypoint barrels. | | no-stubbed-subject | Disallow a <stem>.test file from replacing behavior of its own <stem> module through vi.spyOn(...) followed by a canned return value or implementation. | | no-test-logic-in-production | Disallow production files from branching on a test runner or test mode and from exporting members named or commented as existing only for tests. | | no-test-sleeps | Disallow wall-clock sleeps in test files: setTimeout-backed promises, node:timers/promises sleeps, and awaited sleep helpers with a fixed duration. | | no-vitest-in-source | Disallow importing vitest from non-test source files. | | no-vitest-mocking | Disallow Vitest mocking APIs in favor of deterministic test doubles. | | no-weak-test-assertions | Disallow individual tests whose only assertions check existence, a bare call, a typeof or Object instance, wildcard-only matcher arguments, a value against itself or a literal against a literal, or a local mock's own return value. | | padding-before-exit | Require a blank line before a return or throw statement that follows another statement in the same block, switch case, or program body. | | padding-line-between-statements | Require or disallow blank lines between statements, with ESLint's options and a comment block counted as part of the statement below it; defaults to padding blocks, multiline expressions, declaration runs, and exits. |

Credited concepts

  • "Eradicating Non-Determinism in Tests" by Martin Fowler (concept)
  • "Test Logic in Production" (G. Meszaros, xUnit Test Patterns) (concept)
  • code-slop by asyrafhussin (MIT, concept re-implemented)
  • code-slop by asyrafhussin (MIT, concept re-implemented) — closing-brace labels and placeholder
  • https://eslint.org/docs/latest/rules/padding-line-between-statements (MIT; concept, independently implemented)
  • https://eslint.org/docs/latest/rules/padding-line-between-statements (MIT; option schema and statement types, independently implemented)