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

@fohte/eslint-config

v0.5.2

Published

ESLint config for fohte

Downloads

2,561

Readme

@fohte/eslint-config

Personal ESLint configuration package with TypeScript support.

Installation

npm install --save-dev @fohte/eslint-config

# Install peer dependencies
npm install --save-dev @eslint-community/eslint-plugin-eslint-comments @typescript-eslint/eslint-plugin @typescript-eslint/parser @vitest/eslint-plugin eslint eslint-config-prettier eslint-plugin-import-x eslint-plugin-simple-import-sort

# Optional: If using TypeScript
npm install --save-dev typescript

# Optional: If using the errorHandling option
npm install --save-dev @ninoseki/eslint-plugin-neverthrow
npm install neverthrow

# Optional: If using the tailwind option
npm install --save-dev eslint-plugin-tailwindcss
npm install tailwindcss

# Optional: If using the shadcn option
npm install --save-dev @shadcn/lint

Usage

eslint.config.js:

import { config } from '@fohte/eslint-config'

// Basic (JS + TypeScript strict rules)
export default config()

// Alternatively, enable type-checked rules
// (strict-type-checked + strict-boolean-expressions):
// export default config({ typescript: { typeChecked: true } })

// Optionally, ban throw/try-catch and enforce neverthrow Result handling
// (requires typescript.typeChecked: true):
// export default config({
//   typescript: { typeChecked: true },
//   errorHandling: {},
// })

// Optionally, ban raw tracer.startSpan()/startActiveSpan() calls that skip context.with():
// export default config({ opentelemetry: { enabled: true } })

// Optionally, ban Tailwind CSS arbitrary values (e.g. `w-[600px]`),
// steering towards design tokens defined in `@theme` instead:
// export default config({
//   tailwind: { cssConfigPath: 'src/index.css' },
// })

// Optionally, enforce shared component styling rules in selected files:
// export default config({
//   shadcn: { files: ['src/**/*.{jsx,tsx}'] },
// })

// Optionally, ban raw form and button elements in selected files:
// export default config({
//   noRawFormElements: {
//     files: ['app/src/**/*.tsx'],
//     ignores: ['app/src/components/ui/**'],
//   },
// })

Import policy

config() bans relative imports (./foo, ../foo) and the @/* alias via no-restricted-imports, and steers both towards a Node subpath import (#foo, declared under the imports field in package.json). The @/* alias only resolves at the TypeScript/bundler level, so a plain Node/tsx runtime that doesn't share that resolution step fails at runtime; # imports are resolved natively by Node's own module resolver, so this failure mode can't happen.

To opt out (e.g. for a package that doesn't use a src/ layout), override the rule in a trailing userConfigs argument passed to config():

export default config(
  {},
  {
    rules: {
      'no-restricted-imports': 'off',
    },
  },
)

errorHandling option

Requires typescript.typeChecked: true, because neverthrow/must-use-result needs type information to detect unused Result values.

When enabled, it applies two rules to all .ts{,x} files except test files:

  • no-restricted-syntax: bans throw and try/catch. Return a Result via err()/errAsync() instead, or use ResultAsync.fromPromise() to interop with a throwing API without a local throw. If an external SDK's throw-based contract genuinely can't be wrapped that way, add an eslint-disable-next-line comment explaining why:

    // eslint-disable-next-line no-restricted-syntax -- interops with an external SDK's throw-based contract
    try {
      return externalSdkCall()
    } catch (error) {
      return err(error)
    }
  • neverthrow/must-use-result: bans discarding a neverthrow Result/ResultAsync without handling it.

opentelemetry option

When enabled, no-restricted-syntax bans direct calls to tracer.startSpan()/tracer.startActiveSpan() on all .ts{,x} files, since both can silently produce a span that fails to parent child spans created during its execution:

  • startSpan() never enters the active context on its own — a child span (e.g. an HTTP call fired during this span) won't be nested under it unless the caller explicitly wraps the surrounding code in context.with(trace.setSpan(context.active(), span), ...).
  • startActiveSpan()'s callback runs inside the active context automatically, but only for the callback's own duration — storing the span to end() it later (e.g. start and end split across separate callbacks) drops it from the active context before that later code runs.

This surfaces only as a mis-parented span in a trace backend, not as a runtime error. If neither pattern fits, add an eslint-disable-next-line comment explaining why.

When combined with errorHandling, both options configure no-restricted-syntax — ESLint's flat config fully replaces a rule's settings (rather than merging them) when two config objects set the same rule for the same file, so the startSpan/startActiveSpan selectors are merged into errorHandling's no-restricted-syntax entry instead of their own config. This means the opentelemetry ban then shares errorHandling's exemption for test files too.

tailwind option

cssConfigPath (the path to the CSS file where Tailwind's @theme tokens are defined) is required; files defaults to all .ts{,x} files, but can be narrowed (e.g. a single package in a monorepo):

export default config({
  tailwind: {
    files: ['web/**/*.ts', 'web/**/*.tsx'], // optional, defaults to all .ts{,x} files
    cssConfigPath: 'web/src/index.css',
  },
})

When enabled, it applies two rules to the given files (except test files):

  • tailwindcss/no-arbitrary-value: bans arbitrary values (e.g. w-[600px]) in className/class attributes and classname functions (clsx, cn, etc). Add a token to @theme in your CSS config instead:

    @theme {
      --width-panel: 600px;
    }
    - <div className="w-[600px]" />
    + <div className="w-panel" />
  • no-restricted-syntax: no-arbitrary-value only checks class attributes/functions, so a bracket value stashed in a bare string constant (then interpolated into className) would otherwise slip through undetected. This rule catches that case too.

Like opentelemetry, this shares its no-restricted-syntax entry with errorHandling (and opentelemetry) rather than silently overriding it, so all three bans keep applying together within tailwind.files. Since tailwind.files can be narrower than errorHandling/opentelemetry's default (all .ts{,x} files), the merge happens at tailwind.files's scope: files outside it keep only the errorHandling/opentelemetry bans, and files inside it get the full union.

noRawFormElements option

Set files to the files where raw JSX form and button elements should be banned. Test files are excluded automatically; use ignores for additional exclusions such as shared UI components:

export default config({
  noRawFormElements: {
    files: ['app/src/**/*.tsx'],
    ignores: ['app/src/components/ui/**'],
  },
})

This enables fohte/no-raw-form-elements, which flags lowercase <button>, <input>, <select>, and <textarea> elements and asks you to use a shared UI component instead. <a> elements and capitalized components are unaffected.

shadcn option

Install @shadcn/lint 0.1.2 or later to opt in. Select supported source files with files; test files are excluded automatically, and ignores can add more exclusions:

export default config({
  shadcn: { files: ['src/**/*.{jsx,tsx}'] },
})

This enables shadcn/no-restyle, shadcn/no-inline-styles, shadcn/require-static-classes, shadcn/no-unknown-classes, and shadcn/no-raw-colors as errors. no-restyle recognizes components imported from @fohte/ui and its subpaths, while allowing layout classes. Add a trailing userConfigs entry to override or disable an individual rule. For example, disable no-restyle in a repository's shared component definitions while keeping the other rules enabled:

export default config(
  { shadcn: { files: ['src/**/*.{jsx,tsx}'] } },
  {
    files: ['src/components/ui/**'],
    rules: { 'shadcn/no-restyle': 'off' },
  },
)

Built-in rules

In addition to the upstream presets, this config ships a local plugin (fohte) applied to test files and Storybook story files, and optionally to the files selected by noRawFormElements. Rules enabled by the built-in presets are set to error; rules that require an explicit option or user config are documented below.

fohte/require-story-name is registered for Storybook story files but is off by default. Enable it with a trailing userConfigs entry:

export default config(
  {},
  {
    files: ['**/*.stories.tsx'],
    rules: { 'fohte/require-story-name': 'error' },
  },
)
  • fohte/no-inline-object-in-expect (test files): flags expect(<object/array literal>).toEqual(...) (and toStrictEqual / toMatchObject, including await … .resolves / .rejects / .not chains, and as const / satisfies / ! wrapped literals). Also flags the same literal aliased through a variable declared right before the expect() call. Pass the value under test directly, or split the assertion into multiple expect() calls.

    // bad
    expect({ result, calls: spy.mock.calls.length }).toEqual({
      result: 'ok',
      calls: 0,
    })
    
    // bad: aliasing the literal through a variable doesn't escape the rule
    const actual = { result, calls: spy.mock.calls.length }
    expect(actual).toEqual({ result: 'ok', calls: 0 })
    
    // good
    expect(result).toBe('ok')
    expect(spy).not.toHaveBeenCalled()
  • fohte/no-raw-form-elements (when noRawFormElements is configured): flags lowercase JSX <button>, <input>, <select>, and <textarea> elements. Use the shared UI component instead.

  • fohte/no-play-in-stories (story files): flags a play function and parameters: { screenshot: { skip: true } } on a story or its meta (default export). A story should represent a visual state through args/render only; move behavioral assertions (clicks, input, expect calls) to a .test.tsx file, and render the state a play function used to set up (e.g. an open menu) through a prop instead (e.g. defaultOpen). This also targets a Storybook VRT setup that flags two stories in the same file whose screenshots are byte-identical (usually an undetected visual bug, e.g. two states rendering identically): a skipped story emits no screenshot, so that check can never see it. Since stories can't define play, every story's only assertion is its rendered appearance, so screenshot.skip always hides that finding — fix the underlying visual duplication instead of skipping.

    // bad: play belongs in a .test.tsx file, not the story
    export const Disabled: Story = {
      args: { disabled: true },
      play: async ({ canvasElement }) => {
        await expect(within(canvasElement).getByRole('checkbox')).toBeDisabled()
      },
    }
    
    // good: the state play used to set up is driven by a prop instead
    export const Disabled: Story = {
      args: { disabled: true },
    }
  • fohte/require-story-name (story files, off by default): requires each exported object story to define its own name property. It honors literal string and regular expression filters in includeStories and excludeStories. If either filter is dynamic, the rule skips that file because it cannot determine which exports Storybook treats as stories.

Development

Setup

# Install dependencies
npm install

# Build TypeScript
npm run build

# Watch mode
npm run watch

Scripts

  • npm run build - Compile TypeScript files
  • npm run watch - Watch mode for development
  • npm run lint - Run ESLint on source files
  • npm run test - Run build and lint

Project Structure

src/
├── index.ts           # Main export
├── main.ts            # Base ESLint configuration
├── typescript.ts      # TypeScript-specific configuration
├── error-handling.ts  # errorHandling option (throw/try-catch ban, neverthrow enforcement)
├── opentelemetry.ts   # opentelemetry option (startSpan/startActiveSpan ban)
├── no-raw-form-elements.ts # noRawFormElements option (raw JSX element ban)
├── tailwind.ts        # tailwind option (Tailwind arbitrary-value ban)
├── shadcn.ts          # shadcn option (shared component style rules)
└── types/             # Type definitions for untyped packages

Release Process

This project uses release-please for automated releases.

1. Create a feature branch and make changes

  • Create a new branch from master
  • Make your changes (commit messages don't need to follow any specific format)

2. Create a PR and merge to master

  • Push your branch and create a PR
  • PR title must follow Conventional Commits:
    • fix: for bug fixes (patch release)
    • feat: for new features (minor release)
    • feat!: or fix!: for breaking changes (major release)
  • After review, merge the PR (squash merge only)

3. Automated release process

When changes are merged to master, release-please automatically:

  • Creates/updates a Release PR
  • Updates version in package.json
  • Updates CHANGELOG.md
  • When the Release PR is merged:
    • Creates GitHub release and git tag
    • Publishes to npm

Pre-commit Hooks

This project uses pre-commit hooks to ensure code quality. The hooks will automatically run when you commit changes.