@fohte/eslint-config
v0.4.1
Published
ESLint config for fohte
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 tailwindcssUsage
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' },
// })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: bansthrowandtry/catch. Return aResultviaerr()/errAsync()instead, or useResultAsync.fromPromise()to interop with a throwing API without a localthrow. If an external SDK's throw-based contract genuinely can't be wrapped that way, add aneslint-disable-next-linecomment 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 aneverthrowResult/ResultAsyncwithout 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 incontext.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 toend()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]) inclassName/classattributes and classname functions (clsx,cn, etc). Add a token to@themein your CSS config instead:@theme { --width-panel: 600px; }- <div className="w-[600px]" /> + <div className="w-panel" />no-restricted-syntax:no-arbitrary-valueonly checks class attributes/functions, so a bracket value stashed in a bare string constant (then interpolated intoclassName) 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.
Built-in rules
In addition to the upstream presets, this config ships a local plugin (fohte) for test files. Rules are enabled as error by default; override them in eslint.config.js if needed (e.g. 'fohte/no-inline-object-in-expect': 'off').
fohte/no-inline-object-in-expect: flagsexpect(<object/array literal>).toEqual(...)(andtoStrictEqual/toMatchObject, includingawait … .resolves/.rejects/.notchains, andas const/satisfies/!wrapped literals). Also flags the same literal aliased through a variable declared right before theexpect()call. Pass the value under test directly, or split the assertion into multipleexpect()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()
Development
Setup
# Install dependencies
npm install
# Build TypeScript
npm run build
# Watch mode
npm run watchScripts
npm run build- Compile TypeScript filesnpm run watch- Watch mode for developmentnpm run lint- Run ESLint on source filesnpm 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)
├── tailwind.ts # tailwind option (Tailwind arbitrary-value ban)
└── types/ # Type definitions for untyped packagesRelease 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!:orfix!: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.
