@jabworks/oxlint-config
v0.6.0
Published
Opinionated oxlint presets for JS, TS, React, React Native, Next.js, and Vitest, ported from @jabworks/eslint-plugin
Maintainers
Readme
@jabworks/oxlint-config
Opinionated oxlint presets ported from @jabworks/eslint-plugin.
Presets
| Preset | Extends | Use for |
| ------------- | ------------- | ------------------------------------------------------ |
| base | — | Any JS/TS project |
| typescript | base | TypeScript-only additions (some type-aware, see below) |
| react | typescript | React libraries and apps |
| next | react | Next.js applications |
| reactNative | typescript | React Native apps, bare or any framework (oxlint-only) |
| expo | reactNative | Expo apps, with expo-router conventions (oxlint-only) |
| node | typescript | Node.js backends and APIs |
| library | typescript | Framework-agnostic utility libraries |
| vitest | — | Vitest test files (composable overlay) |
Installation
npm install -D oxlint oxlint-tsgolint @jabworks/oxlint-configUsage
// oxlint.config.ts — pick the preset that matches your project
import { next } from '@jabworks/oxlint-config';
// or: import { expo, node, library, react, reactNative, typescript } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [next],
});// package.json
{
"scripts": {
"lint": "oxlint --type-aware --deny-warnings",
},
}Note:
oxlint.config.tsrequires Node.js v22.18+ or v24+.
To compose or override, use the exported mergeConfigs helper:
import { mergeConfigs, typescript } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
const custom = mergeConfigs(typescript, {
rules: { 'no-console': 'warn' },
});
export default defineConfig({ extends: [custom] });What survives extends
oxlint's extends keeps a preset's rules, categories, plugins, and overrides, but drops its top-level env,
globals, and ignorePatterns (verified on oxlint 1.85, and not mentioned in oxlint's docs). The presets are built
around that:
- Globals (
env.browserinreact/next,env.nodeinnode, the React Native globals inreactNative) are delivered through a catch-allfiles: ['**/*']override, which does surviveextends. - Tool config files (
*.config.{js,mjs,cjs,ts,mts,cts}, includingoxlint.config.tsitself) get an override inbasethat allows their default export. - Ignore patterns (
dist/,build/,coverage/,.next/,.expo/, …) only apply when a preset is spread into your config or passed throughmergeConfigs. With plainextends, rely on.gitignore, which oxlint honors by default and which normally covers these paths already, or addignorePatternsto your own config.
Type-aware rules
The typescript preset enables type-aware rules (typescript/no-misused-promises, typescript/consistent-type-exports, typescript/restrict-template-expressions, …). These require oxlint-tsgolint installed in the consuming project and running oxlint with --type-aware; without it they are silently skipped.
React Native and Expo presets
Neither preset has an @jabworks/eslint-plugin counterpart.
reactNative
For any React Native app, bare or built on a framework. It carries the same React rules as react (hooks, React
Compiler rules, component style) but leaves out everything that assumes a DOM: the jsx-a11y plugin, env.browser,
react/button-has-type, and react/jsx-no-target-blank. On top of that:
- Globals. oxlint has no react-native environment, so the preset enables
shared-node-browser(fetch, timers, URL, console) and declares__DEV__andprocess(the bundler inlinesprocess.envreads) as read-only globals.windowanddocumentstay undefined. - Ignores.
android/andios/.
Platform suffixes such as card.ios.tsx already satisfy unicorn/filename-case in kebab case.
// oxlint.config.ts
import { reactNative } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({ extends: [reactNative] });expo
reactNative plus expo-router's file conventions. It is a separate preset because the route override below would
misfire in a bare React Native app, where an app/ folder holds ordinary named-export modules.
- expo-router routes. Every file under
app/orsrc/app/is a route, layout, or special file (_layout,+not-found,+html) that expo-router loads through its default export. There,import/no-default-exportis off andimport/prefer-default-exportis on. API routes (*+api.ts) export named HTTP handlers and keep the house named-export rule.*.config.{ts,mts,cts}files, such asapp.config.ts, must default-export too. - Ignores.
.expo/andexpo-env.d.ts.
expo-router's file names ([id].tsx, [...rest].tsx, +not-found.tsx, (tabs)/_layout.tsx) already satisfy
unicorn/filename-case in kebab case.
// oxlint.config.ts
import { expo } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({ extends: [expo] });Note: like every preset's ignore patterns, these only apply when the preset is spread or merged (see What survives
extends). A default React Native or Expo.gitignorealready covers these paths.
Imperative renderers (three.js / React Three Fiber)
Code that drives an imperative renderer from React (three.js through React Three Fiber, Skia, expo-gl) trips two React Compiler rules on purpose:
react/refsflags the "latest ref" pattern (ref.current = valueduring render), which frame loops use to read fresh props without re-subscribing every frame.react/preserve-manual-memoizationonly matters when the React Compiler runs, and it is usually off for such code.
imperativeRenderOverride turns both off for the globs you pass. Keep them narrow, so the rest of the app is still
checked:
// oxlint.config.ts
import { imperativeRenderOverride, reactNative } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({
extends: [reactNative],
// Point it at wherever your renderer code lives.
overrides: [imperativeRenderOverride(['src/3d/**'])],
});It works with any React preset, not only reactNative. R3F's JSX props (args, castShadow, …) need no override:
react/no-unknown-property is in oxlint's restriction category, which no preset enables.
Opt-in plugin layers: expoPlugin and reactNativePlugin
Two layers add rules that oxlint does not have built in, by running an ESLint-style plugin through oxlint's JS plugin support. They are opt-in because each one needs its plugin installed, and because any JS plugin adds a fixed runtime cost to every lint run (about 0.2 s on a mid-sized app).
| Layer | Install | Rules |
| ------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| expoPlugin | eslint-plugin-expo (official, from Expo) | no-dynamic-env-var, no-env-var-destructuring, use-dom-exports (error) |
| reactNativePlugin | oxlint-plugin-react-native | no-raw-text (error); no-color-literals, no-inline-styles, no-single-element-style-arrays, no-unused-styles (warn) |
- Env vars: Expo inlines only literal
process.env.EXPO_PUBLIC_*reads, so a dynamic key or a destructured read isundefinedat runtime. no-raw-text: a string outside<Text>is a runtime error on native. The other React Native rules are hygiene: colours belong to theme tokens, and inline or single-element style arrays re-render needlessly.- Left out:
expo/prefer-box-shadow(a styling preference) andreact-native/sort-styles(only orders style keys).
npm install -D eslint-plugin-expo oxlint-plugin-react-native// oxlint.config.ts
import { expo, expoPlugin, reactNativePlugin } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
// A bare React Native app would use `reactNative` with `reactNativePlugin` alone.
export default defineConfig({ extends: [expo, expoPlugin, reactNativePlugin] });Both plugins are optional peer dependencies of this package, which lets pnpm link them where the preset can resolve
them. Opting into a layer without installing its plugin stops the lint run with Failed to load JS plugin: <name>. JS
plugins are alpha in oxlint and outside its semver guarantees, so pin oxlint in apps that use them.
Node and library presets
node
Sets env.node: true and enables the built-in node plugin with Node.js best practices:
unicorn/no-process-exit— useprocess.exitCodeinstead ofprocess.exit()(registered under oxlint'sunicornplugin)node/no-path-concat— usepath.join()orpath.resolve()over string concatenationnode/no-new-require— disallownew require(…)node/no-exports-assign— disallow reassigningexportsnode/handle-callback-err— enforce error handling in callbacks
Also includes a vitest test-file override (same as react).
// oxlint.config.ts
import { node } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({ extends: [node] });Note:
n/prefer-promises/fs,n/prefer-promises/dns, andn/prefer-global/*fromeslint-plugin-nhave no oxlint built-in equivalent as of 1.85. Use the ESLintnodeconfig alongside for those rules.
library
No environment globals — suitable for framework-agnostic packages (utility libraries, shared hooks, etc.). Activates import/no-cycle which is disabled in base for performance.
// oxlint.config.ts
import { library } from '@jabworks/oxlint-config';
import { defineConfig } from 'oxlint';
export default defineConfig({ extends: [library] });Rules not ported from @jabworks/eslint-plugin
Unsupported by oxlint 1.85. Verify by loading a config that names the rule and
checking for Rule '<name>' not found in plugin '<plugin>' — oxlint --rules
prints nothing and cannot be used for this.
- Core:
no-octal-escape,camelcase,no-undef-init - Import:
no-extraneous-dependencies,no-relative-packages,no-useless-path-segments,no-deprecated - TypeScript:
naming-convention - React:
jsx-no-leaked-render,jsx-sort-props,react-hooks/config,react-hooks/gating(the other twelve React Compiler rules fromreact-hooksrecommended-latestare ported, at the same severities) - Node:
n/prefer-promises/fs,n/prefer-promises/dns,n/no-callback-literal,n/prefer-global/* - Plugins with no oxlint equivalent:
simple-import-sort,eslint-comments,@stylistic(formatting is Prettier's job)
Restored in oxlint 1.59–1.75 (previously dropped): no-implied-eval,
prefer-regex-literals, prefer-arrow-callback, no-unreachable-loop,
typescript/method-signature-style, object-shorthand,
import/newline-after-import, import/prefer-default-export,
react/function-component-definition, react/hook-use-state,
react/no-unstable-nested-components.
