@wistia/oxlint-config
Wistia's Oxlint configurations. This is a shared config package for oxlint, a fast TypeScript/Javascript linter.
Philosophy & Principles
- Always Error, Never Warn: Warnings become background noise that developers tune out. A rule should either flag a real problem or stay silent.
- Strict, Consistent Code Style: Where there's more than one way to do something, this configuration picks the strictest and most uniform option, favoring modern syntax and established best practices.
- Fast: Known performance-heavy rules are skipped.
- Don't get in the way: Rules that involve style preferences are intentionally disabled as this is best left to a formatter like
prettier. Whenever possible, rules that can auto-fix are chosen to minimize friction and save developer time.
How to install
yarn add -D @wistia/oxlint-config oxlint eslint
Why
eslint? This package uses oxlint's jsPlugins feature to run ESLint plugins (e.g.eslint-plugin-import-x,@eslint-react/eslint-plugin,eslint-plugin-testing-library, etc.) natively inside oxlint. These plugins import utilities from theeslintpackage at runtime, so it must be installed even though you won't runeslintdirectly.Note: jsPlugins do not support rules that rely on TypeScript type information. Any type-aware rules in this config use native oxlint rules instead.
Type-aware linting
The typescriptConfig enables type-aware linting, which requires the oxlint-tsgolint package:
yarn add -D oxlint-tsgolint
oxlint-tsgolint is a separate Go-based tool that builds your TypeScript program using typescript-go and runs type-aware rules (e.g. detecting unhandled promises, unsafe assignments). Without it installed, type-aware rules will not run.
Note: this will likely be unnecessary in the future when Typescript v7 is released.
Quick start
Create an oxlint.config.ts in your project root:
TypeScript & React project (most common):
import { defineConfig } from 'oxlint';
import { typescriptConfig, reactConfig } from '@wistia/oxlint-config';
export default defineConfig({
extends: [typescriptConfig, reactConfig],
});
JavaScript-only project:
import { defineConfig } from 'oxlint';
import { javascriptConfig } from '@wistia/oxlint-config';
export default defineConfig({
extends: [javascriptConfig],
});
Available configs
Base configs
| Export | Description |
|---|---|
javascriptConfig |
Base config for any JS project (base + import + promise + barrel-files rules) |
typescriptConfig |
TypeScript projects (includes all JavaScript rules + TypeScript rules) |
Feature configs
Feature configs wrap their rules in overrides with default file patterns, so they scope correctly when extended at the top level.
| Export | Default file patterns | Description |
|---|---|---|
reactConfig |
all files (global) | React component + accessibility rules |
styledComponentsConfig |
all files (global) | Styled Components accessibility rules |
nodeConfig |
**/*.{mts,mjs,cjs} |
Node.js-specific rules |
vitestConfig |
**/*.{test,vitest}.{ts,tsx,js,jsx} |
Vitest testing rules |
testingLibraryConfig |
**/*.{test,vitest}.{ts,tsx,js,jsx} |
Testing Library + jest-dom rules |
playwrightConfig |
**/*.spec.{ts,tsx,js}, **/e2e/**/*.ts |
Playwright E2E testing rules |
storybookConfig |
**/*.stories.{ts,tsx,js,jsx} |
Storybook story conventions |
Composing configs:
import { defineConfig } from 'oxlint';
import {
typescriptConfig,
reactConfig,
styledComponentsConfig,
vitestConfig,
testingLibraryConfig,
storybookConfig,
nodeConfig,
} from '@wistia/oxlint-config';
export default defineConfig({
extends: [
typescriptConfig,
reactConfig,
styledComponentsConfig,
vitestConfig,
testingLibraryConfig,
storybookConfig,
nodeConfig,
],
});
Overriding rules
Add a rules section — your rules take precedence over the shared configs:
export default defineConfig({
extends: [typescriptConfig, reactConfig],
rules: {
'typescript/no-explicit-any': 'off',
'react/no-clone-element': 'off',
},
});
File-specific overrides use overrides. Note that extends is not supported inside overrides — use rule imports for those:
import { defineConfig } from 'oxlint';
import { typescriptConfig, vitestRules } from '@wistia/oxlint-config';
export default defineConfig({
extends: [typescriptConfig],
overrides: [
{
files: ['**/*.test.ts', '**/*.vitest.ts'],
plugins: vitestRules.plugins,
jsPlugins: vitestRules.jsPlugins,
rules: {
...vitestRules.rules,
// project-specific overrides
'jest/expect-expect': ['error', { assertFunctionNames: ['expect', 'expectValid'] }],
},
},
],
});
Running oxlint
{
"scripts": {
"lint:oxlint": "oxlint -c oxlint.config.ts ."
}
}
Formatting with oxfmt
This package also ships Wistia's shared oxfmt config as oxfmtConfig, exported from the @wistia/oxlint-config/oxfmtConfig subpath. It is only tangentially related to linting, so oxfmt is an optional peer dependency — install it only if you use the formatter:
yarn add -D oxfmt
Create an oxfmt.config.mts in your project root. Spread oxfmtConfig and override any field; use satisfies OxfmtConfig for type checking:
import type { OxfmtConfig } from 'oxfmt';
import { defineConfig } from 'oxfmt';
import { oxfmtConfig } from '@wistia/oxlint-config/oxfmtConfig';
// oxlint-disable-next-line import-x/no-default-export
export default defineConfig({
...oxfmtConfig,
printWidth: 120,
ignorePatterns: [...oxfmtConfig.ignorePatterns, 'dist/**'],
} satisfies OxfmtConfig);
Every field is overrideable. Because oxfmtConfig is a plain object, spread it and replace whichever keys you need — nested keys such as sortImports and ignorePatterns are replaced wholesale, so reference the base value (as above) when you only want to extend it.
To use the config as-is with no overrides, point oxfmt at the package's default export directly:
// oxfmt.config.mts
export { default } from '@wistia/oxlint-config/oxfmtConfig';
Add a script to run it:
{
"scripts": {
"format": "oxfmt .",
"format:ci": "oxfmt --check ."
}
}
Guidelines for adding new rules
- Preference given for autofixable rules
- Should not contradict existing rules
- Person/team adding new rules handles upgrading consumers and fixing violations
- Rules should always be set to
error, neverwarn - Add short description of rule and link to rule documentation in code comments
- Never delete a rule — set it to
offwith a comment explaining why
Why some rules are ESLint-only
oxlint's jsPlugins feature can load ESLint plugins that export a standard { rules } object. This works for packages like eslint-plugin-import-x, eslint-plugin-storybook, etc.
Several categories of rules cannot use this approach:
Core ESLint rules (e.g.
no-restricted-globals,camelcase,object-shorthand). These are built into theeslintpackage, not exported as a plugin with a{ rules }object.@eslint/jsonly exports configs, not rules. There is no standalone ESLint plugin that re-exports core rules in the format oxlint's jsPlugins expect.Rules requiring ESLint parser services (e.g.
@eslint-react/*,@typescript-eslint/*). These callgetParserServices()from@typescript-eslint/utilsinternally, which requires ESLint's parser infrastructure. oxlint's jsPlugin runner does not provide this.Rules requiring module resolution (e.g.
import-x/no-unresolved,import-x/extensions,import-x/no-rename-default). In ESLint, these rules useeslint-import-resolver-typescriptto resolve TypeScript path aliases, barrel re-exports, and extensionless imports. The resolver is configured via ESLint'ssettings['import-x/resolver']— but oxlint's jsPlugin runner does not pass ESLint settings to plugins. Without a resolver, these rules can't find modules and produce thousands of false positives on every import.Old-style ESLint plugins (e.g.
eslint-plugin-filenames). These export rules as plain functions instead of objects with acreatemethod, which oxlint's jsPlugin runner — requiring the modern{ meta, create }format — cannot load. The package is also abandoned. Where a rule from such a plugin is worth keeping, it is reimplemented in our own custom local jsPlugin (custom) underplugins/custom.mjs.custom/match-exportedis a faithful port ofeslint-plugin-filenames'match-exported, enabled injavascriptConfig+typescriptConfig, matching eslint-config. (Its rule ID is namespacedcustom/rather thanfilenames/to signal it is our own rule, not the upstream plugin.) The upstreammatch-regexandno-indexrules are not ported — eslint-config disables both.