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

@fookiejs/eslint-plugin

v0.1.10

Published

ESLint plugin for TypeScript code quality — naming, structure, type safety.

Readme

fookie-eslint

fookie-eslint-plugin — Opinionated ESLint rules for TypeScript enforcing explicit types, immutable data flow, and zero JS gotchas.

Installation

npm install --save-dev fookie-eslint-plugin

Setup

import fookie from 'fookie-eslint-plugin'

export default [
  fookie.configs['recommended'],
  {
    languageOptions: {
      parserOptions: {
        project: './tsconfig.json',
      },
    },
  },
]

Set "useUnknownInCatchVariables": false in tsconfig.json. Catch bindings must stay unannotated — TypeScript only allows any / unknown annotations there, and both are forbidden by this plugin. Pair with no-throw-literal so the runtime value is always an Error.


Rules

Type Safety

no-any

Disallow the any type.

// ❌
const user: any = fetchUser(profileUrl)

// ✅
const user: User = await fetchUser(profileUrl)

no-type-assertion

Disallow as type assertions. Narrow with domain type predicates — do not mix unrelated types in one union.

// ❌
const user = httpPayload as User

// ✅
function isRegisteredUser(account: Account): account is RegisteredUser {
  return account instanceof RegisteredUser
}

no-non-null-assertion

Disallow non-null assertions (!).

// ❌
queryAppRoot('app')!.innerHTML = ''

// ✅
const appRoot = queryAppRoot('app')
appRoot.replaceChildren()

same-type-comparison

Both sides of === must have the same TypeScript type.

// ❌
if (count === '5') { }

// ✅
if (count === 5) { }

Error Handling

no-throw-literal

Only Error instances can be thrown.

// ❌
throw 'something went wrong'
throw { message: 'oops' }

// ✅
throw new Error('something went wrong')

no-catch-unknown

Disallow explicit unknown annotation on catch bindings. Leave the binding unannotated. Pair with no-throw-literal and useUnknownInCatchVariables: false.

// ❌
catch (caughtError: unknown) { }

// ✅
catch (caughtError) {
  reportFailure(caughtError)
}

no-catch-instanceof

Disallow instanceof Error checks inside catch blocks. Pair with no-throw-literal — pass the caught value to an Error-typed handler.

// ❌
catch (caughtError) {
  if (caughtError instanceof Error) {
    console.log(caughtError.message)
  }
}

// ✅
catch (caughtError) {
  reportFailure(caughtError)
}

no-floating-promise

Promises must be awaited, returned, or .catch()-handled.

// ❌
saveUser(user)

// ✅
await saveUser(user)

Immutability

no-array-mutating-methods

Disallow in-place array mutation methods. Use non-mutating copies such as toSorted / toReversed, or pure derivations.

// ❌
orderLines.sort()
orderLines.reverse()
orderLines.splice(0, 1)

// ✅
orderLines.toSorted()
orderLines.toReversed()
orderLines.filter((_orderLine, orderIndex) => orderIndex !== 0)

no-param-reassign

Disallow reassignment of function parameters.

// ❌
function totalWithTax(amount: number): number {
  amount = amount * 1.2
  return amount
}

// ✅
function totalWithTax(amount: number): number {
  return amount * 1.2
}

no-delete

Disallow the delete operator.

// ❌
delete record.key

// ✅
const recordWithoutKey = record.withoutKey()

no-mutable-exports

Disallow exporting let variables.

// ❌
export let count = 0

// ✅
export const count = 0

no-class-mutation

Disallow this.prop = assignments outside the constructor.

// ❌
class ScoreBoard {
  update(): void { this.score = 42 }
}

// ✅
class ScoreBoard {
  private constructor(readonly score: number) {}
  static create(score: number): ScoreBoard {
    return new ScoreBoard(score)
  }
  withScore(nextScore: number): ScoreBoard {
    return new ScoreBoard(nextScore)
  }
}

prefer-readonly-params

Array parameters must be typed as readonly.

// ❌
function sumAmounts(amounts: number[]): number

// ✅
function sumAmounts(amounts: readonly number[]): number

Control Flow

require-boolean-condition

Conditions in if/while/ternary must be explicitly boolean.

// ❌
if (user) { }
if (orderLines.length) { }

// ✅
if (user.isActive === true) { }
if (orderLines.length > 0) { }

exhaustive-switch

Switch over union types must cover all members without a default fallback.

// ❌
type AccountStatus = 'active' | 'inactive' | 'banned'
switch (accountStatus) {
  case 'active': return 'Active'
  case 'inactive': return 'Inactive'
}

// ✅
switch (accountStatus) {
  case 'active': return 'Active'
  case 'inactive': return 'Inactive'
  case 'banned': return 'Banned'
}

consistent-return

Functions must either always or never return a value.

// ❌
function getStatusLabel(accountStatus: string) {
  if (accountStatus === 'active') {
    return 'Active'
  }
}

// ✅
function getStatusLabel(accountStatus: string): string {
  if (accountStatus === 'active') {
    return 'Active'
  }
  return 'Unknown'
}

no-async-without-await

Async functions must contain at least one await.

// ❌
async function loadUser(): Promise<User> {
  return db.findUser()
}

// ✅
async function loadUser(): Promise<User> {
  return await db.findUser()
}

no-loop-func

Disallow function definitions inside loops.

// ❌
for (let count = 0; count < 3; count++) {
  setTimeout(() => console.log(count), 0)
}

// ✅
for (const orderLine of orderLines) {
  processOrderLine(orderLine)
}

Naming

no-generic-names

Disallow vague names like data, info, temp, result, obj, item, value, element.

// ❌
const data = await fetchUser()
const temp = data.name

// ✅
const user = await fetchUser()
const displayName = user.name

no-placeholder-names

Disallow placeholder names like foo, bar, baz, qux, dummy.

// ❌
catalogEntries.map(foo => foo.name)

// ✅
catalogEntries.map(catalogEntry => catalogEntry.name)

no-shadow

Disallow variable declarations that shadow outer scope variables.

// ❌
const user = getUser()
users.map(user => user.name)

// ✅
const currentUser = getUser()
users.map(member => member.name)

JS Gotchas

no-for-in

Disallow for...in — it iterates the prototype chain.

// ❌
for (const configKey in config) { }

// ✅
for (const configKey of Object.keys(config)) { }

no-union-type

Disallow TypeScript union types (A | B) everywhere.

// ❌
function normalize(value: string | false): string { return "" }
type IdOrMissing = string | false

// ✅
function normalize(value: string): string { return value }
type EntityId = string

no-implicit-coercion

Disallow implicit type coercions.

// ❌
const parsedAmount = +'42'
const isEnabled = !!rawFlag

// ✅
const parsedAmount = Number('42')
const isEnabled = Boolean(rawFlag)

no-typeof

Disallow typeof — validate with Zod (safeParse), narrow from Models.

// ❌
if (typeof accountStatus === 'string') { }

// ✅
if (accountStatusSchema.safeParse(accountStatus).success === true) { }

no-hasownproperty

Disallow hasOwnProperty / Object.hasOwn — walk entries, Zod-validate, or use Model fields.

// ❌
Object.prototype.hasOwnProperty.call(record, key)

// ✅
for (const [fieldKey, fieldValue] of Object.entries(record)) { usePair(fieldKey, fieldValue) }

no-eval

Disallow eval() and indirect eval patterns.

// ❌
eval('console.log(1)')

// ✅
parseConfig(configSource)

no-new-wrappers

Disallow new String(), new Number(), new Boolean().

// ❌
const greeting = new String('hello')

// ✅
const greeting = 'hello'

prefer-includes

Require .includes() over .indexOf() comparisons.

// ❌
orderLines.indexOf('target') !== -1

// ✅
orderLines.includes('target')

no-string-concat

Disallow + string concatenation. Use template literals.

// ❌
'Hello, ' + displayName + '!'

// ✅
`Hello, ${displayName}!`

no-legacy-globals

Disallow host globals (window, document, global, globalThis), legacy calls (parseInt, isNaN, …), and arguments.

// ❌
window.addEventListener('click', clickHandler)
parseInt(raw, 10)

// ✅
addEventListener('click', clickHandler)
Number.parseInt(raw, 10)

Modules

no-require

Disallow require(). Use ES import.

// ❌
const fs = require('fs')

// ✅
import fs from 'fs'

no-default-export

Disallow default exports. Use named exports.

// ❌
export default function handleRequest() { }

// ✅
export function handleRequest(): void { }

no-process-env

Disallow direct process.env access. Use a typed config module.

// ❌
const port = process.env.PORT

// ✅
import { config } from './config.js'
const port = config.port

Classes

require-private-constructor

Class constructors must be private or protected. Use static factory methods. Use protected on abstract bases that subclasses extend via super().

// ❌
class User {
  constructor(public name: string) {}
}

// ✅
class User {
  private constructor(public name: string) {}
  static create(name: string): User {
    return new User(name)
  }
}

// ✅ base error type (subclasses call super)
class FookieError extends Error {
  protected constructor(message: string) {
    super(message)
  }
  static create(message: string): FookieError {
    return new FookieError(message)
  }
}

no-class-mutation

Disallow this.prop = outside the constructor.

// ❌
class Counter {
  increment(): void { this.count++ }
}

// ✅
class Counter {
  private constructor(readonly count: number) {}
  static create(count: number): Counter {
    return new Counter(count)
  }
  increment(): Counter {
    return new Counter(this.count + 1)
  }
}

Functions

require-explicit-return-type

Exported functions and public class methods must declare return types.

// ❌
export function loadUserById(userId: string) {
  return db.findUserById(userId)
}

// ✅
export function loadUserById(userId: string): Promise<User> {
  return db.findUserById(userId)
}

no-comments

Disallow code comments — write self-documenting code instead.

// ❌
// resolve account from store
const account = await fetchUserById(userId)

// ✅
const account = await fetchUserById(userId)

no-null-undefined

Disallow null, undefined, and void as values. Do not call APIs that return null. Prefer a present value or throw.

// ❌
function findUserById(userId: string): User | null {
  return null
}

// ✅
function loadUserById(userId: string): User {
  return userRepository.loadByIdOrThrow(userId)
}

min-function-lines

Function bodies need at least 7 lines. Tiny snippets elsewhere in this README only illustrate other rules — production methods earn their length with real steps, not rename chains.

// ❌
function loadAccount(): Account {
  return accountRepository.loadOrThrow(accountId)
}

// ✅
function chargeOrder(order: Order): Receipt {
  const pricedOrder = priceOrder(order)
  const taxedOrder = applyTax(pricedOrder)
  const paidOrder = capturePayment(taxedOrder)
  const packedOrder = reserveStock(paidOrder)
  const shippedOrder = scheduleShipment(packedOrder)
  const closedOrder = closeOrder(shippedOrder)
  return issueReceipt(closedOrder)
}

no-unknown

Disallow the unknown type. Use a specific Model or domain union.

// ❌
function parseAccount(raw: unknown): Account {
  return Account.create(raw)
}

// ✅
function parseAccount(raw: AccountDraft): Account {
  return Account.create(raw)
}

no-nullish-operators

Disallow ?? and ?.. Design Models that guarantee presence.

// ❌
const displayName = account.name ?? 'guest'
const city = account.address?.city

// ✅
const displayName = account.name
const city = account.address.city

no-empty-string

Disallow empty string literals ("").

// ❌
const label = ''

// ✅
const label = DEFAULT_LABEL

require-curly

Require curly braces for all control flow statements.

// ❌
if (account.isActive === true) return account

// ✅
if (account.isActive === true) {
  return account
}

NaN

no-nan

Disallow the NaN keyword. Check with Number.isNaN when forced to confront foreign numeric junk — prefer Models that never carry NaN.

// ❌
const broken = NaN

// ✅
if (Number.isNaN(foreignAmount) === true) {
  throw new Error('amount invalid')
}

no-nan-in-math-result

Do not feed Math calls that can yield NaN straight into arithmetic without a guard.

// ❌
const root = Math.sqrt(area) + 1

// ✅
const rootCandidate = Math.sqrt(area)
if (Number.isNaN(rootCandidate) === true) {
  throw new Error('area invalid')
}
const root = rootCandidate + 1

no-parseint-nan

Do not use parseInt / parseFloat results in arithmetic without a NaN guard.

// ❌
const total = parseInt(rawCount, 10) + 1

// ✅
const parsedCount = parseInt(rawCount, 10)
if (Number.isNaN(parsedCount) === true) {
  throw new Error('count invalid')
}
const total = parsedCount + 1

no-nan-array-indexof

Disallow array.indexOf(NaN). Do not put NaN in arrays — redesign the Model.

// ❌
amounts.indexOf(NaN)

// ✅
amounts.includes(targetAmount)

Mutation

no-object-assign-mutation

Disallow Object.assign onto an existing reference.

// ❌
Object.assign(account, patch)

// ✅
const nextAccount = account.withPatch(patch)

no-prototype-mutation

Disallow mutating prototypes at runtime.

// ❌
Account.prototype.tag = 'x'

// ✅
class Account {
  private constructor(readonly tag: string) {}
  static create(tag: string): Account {
    return new Account(tag)
  }
}

no-define-property

Disallow Object.defineProperty / Object.defineProperties.

// ❌
Object.defineProperty(account, 'locked', { value: true })

// ✅
const lockedAccount = account.withLocked(true)

no-map-set-mutation

Disallow mutating Map / Set instances created with new.

// ❌
const registry = new Map()
registry.set(userId, account)

// ✅
const registry = new Map([[userId, account]])

no-spread

Disallow spread and rest (...). Copying bags is not Model work — return a new instance through an explicit method.

// ❌
const nextAccount = { ...account, ...patch }
const { key, ...rest } = record
fn(...args)

// ✅
const nextAccount = account.withPatch(patch)
const recordWithoutKey = record.withoutKey()
fn(firstArg, secondArg)