@fohte/eslint-config

中文文档
0.5.3发布于 2026/10/18MITTypeScriptES Module构建来源已验证近 30 天下载 2718

ESLint config for fohte

原始使用文档
文档目录15 节
  1. Installation
  2. Usage
  3. Import policy
  4. errorHandling option
  5. opentelemetry option
  6. tailwind option
  7. noRawFormElements option
  8. shadcn option
  9. Built-in rules
  10. Development
  11. Setup
  12. Scripts
  13. Project Structure
  14. Release Process
  15. Pre-commit Hooks

@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.

版本与发布记录

@fohte/eslint-config 共发布 27 个版本,首次发布于 2023/12/24;过去 12 个月发布了 21 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.5.3latest2026/10/1829 天前
0.5.22026/10/041 个月前
0.5.12026/09/251 个月前
0.5.02026/09/152 个月前
0.4.42026/09/072 个月前
0.4.32026/08/302 个月前

关于 @fohte/eslint-config 的常见问题

@fohte/eslint-config 是什么?

ESLint config for fohte

如何安装 @fohte/eslint-config?

在项目目录运行 npm install @fohte/eslint-config,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:eslint(^9.0.0 || ^10.0.0)、@vitest/eslint-plugin(^1.0.0)、eslint-config-prettier(^10.0.0)、eslint-plugin-import-x(^4.0.0)、@typescript-eslint/parser(^8.60.0)、@typescript-eslint/eslint-plugin(^8.60.0)、eslint-plugin-simple-import-sort(^13.0.0)、@eslint-community/eslint-plugin-eslint-comments(^4.0.0)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@fohte/eslint-config 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fohte/eslint-config 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fohte/eslint-config 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=20.19.0 || >=22.13.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@fohte/eslint-config 可以商用吗?

@fohte/eslint-config 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@fohte/eslint-config 的下载量有多少?

根据 npm 官方下载统计,@fohte/eslint-config 在 2026/10/15 – 2026/11/13 这 30 天内被下载 2,718 次,日均约 91 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fohte/eslint-config 还在维护吗?

最新版本 0.5.3 发布于 2026/10/18(29 天前);至今共发布 27 个版本,其中过去 12 个月发布了 21 个;首次发布于 2023/12/24。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。