@jeremybarbet/oxlint-plugin
v0.3.0
Published
Shared oxlint JS plugin: dead-code rules that oxlint, tsc and knip each miss, plus a few house rules.
Maintainers
Readme
@jeremybarbet/oxlint-plugin
Shared oxlint JS plugin.
Install
yarn add -D @jeremybarbet/oxlint-plugin// oxlint.config.mts
import { defineConfig } from 'oxlint';
export default defineConfig({
jsPlugins: ['@jeremybarbet/oxlint-plugin'],
rules: {
'op/no-export-all': 'error',
'op/no-unused-modules': 'error',
'op/no-unused-class-members': 'error',
},
});Rules
no-unused-modules
Reports exports nothing else imports, and optionally files with no exports at all. A port of
import/no-unused-modules from eslint-plugin-import.
It overlaps with knip, but the diagnostic lands on the export itself, in the editor, on the same
pass as every other lint rule — rather than as a batch report from a separate command.
Not reported:
- Exports tagged
/** @public */or/** @beta */, the same opt-out knip honours. - Exports consumed inside their own file. Those are over-exposed, not dead — the fix is dropping the
exportkeyword, not deleting the declaration. Most exported return types look like this. - Anything matching
**/*.config.*, always. A config file is loaded by path by the tool that owns it, so its default export is an entry point by definition.
| Option | Default | |
| --- | --- | --- |
| unusedExports | true | Report exports nothing imports. |
| missingExports | false | Report files with no exports. Noisy — every spec and mock qualifies. |
| ignoreExports | [] | Globs whose exports are never reported. Put your entry points here. |
| ignoreDefaultExports | false | Never report export default. Pass globs to scope it — a file-based router like expo-router loads its routes by path, so their default export is an entry point, never an import. |
'op/no-unused-modules': ['error', {
ignoreExports: ['scripts/**'],
ignoreDefaultExports: ['app/**'], // expo-router loads routes by path
}],ignoreDefaultExports is the narrow version of ignoreExports: listing app/** there would also
silence the named exports in those files, so a route that grew a dead helper would go unreported.
no-unused-class-members
Reports public class members that nothing outside the declaring file references.
This is the gap that motivated the package. knip analyses module-level exports only, so an
exported class that is used hides its entire body from it; tsc's noUnusedLocals covers
private members alone. In a codebase where most logic lives on service and controller classes,
that leaves most of the code unchecked.
The check is name-based — oxlint exposes no type checker to JS plugins — so it is deliberately conservative. A member whose name is referenced anywhere else, even on an unrelated class, counts as used. It under-reports rather than pointing at live code.
Not reported:
privateand#privatemembers, and constructor parameter properties.- Any member carrying a decorator. A decorator means something outside the type system reaches it —
a
@Cronschedule, a GraphQL@Field, aclass-validatorconstraint. - Members of classes that
extendorimplementanything, since those can be invoked polymorphically by a base class or by whatever holds the interface. - Members referenced only from test files (mirrors
knip --production).
| Option | Default | |
| --- | --- | --- |
| ignoreNames | framework lifecycle hooks | Member names never reported. |
| ignoreUsagesIn | specs, mocks, src/test/** | Globs whose references do not count as usage. |
| reportPrivateCandidates | false | Also report members used only inside their own file, suggesting private. Off by default: those are a refactor prompt, not dead code. |
no-export-all
Disallows export *, which defeats tree-shaking and invites name clashes.
no-unused-styles
React Native. Reports entries in a StyleSheet.create object that nothing consumes. Understands the
plain object form and both function forms react-native-unistyles uses.
require-cache-control
NestJS + Apollo. Requires @CacheField over @Field for non-scalar GraphQL object type fields, so a
nested type cannot silently break Apollo's cache-control computation. Ships an autofix suggestion.
Project discovery
no-unused-modules and no-unused-class-members need a whole-project view, so they crawl and parse
the project once per process and answer per-file from the result. (Oxlint's plugin API is per-file
with no end-of-run hook, so this is the same approach eslint-plugin-import takes.) It costs roughly
120ms on an 830-file repo.
Discovery is zero-config: everything under the oxlint working directory that looks like source,
skipping node_modules, dist, build, coverage, android, ios and dotfile directories.
Import specifiers are resolved through tsconfig.json paths, including the nodenext .js → .ts
rewrite.
If the defaults are not enough, narrow it via settings. The key is the package name, not the rule
prefix, so it does not move when you alias the plugin:
settings: {
'@jeremybarbet/oxlint-plugin': {
roots: ['src'],
ignore: ['src/api/generated'],
},
},