@rnzeus/eslint-plugin
v0.1.5
Published
ESLint plugin for React / React Native projects focused on **style discipline** and **FSD import boundaries**.
Readme
@rnzeus/eslint-plugin
ESLint plugin for React / React Native projects focused on style discipline and FSD import boundaries.
This plugin is designed to solve real-world problems that appear during refactoring:
- unused styles left in
*.styles.tsfiles - inconsistent style naming
- unsafe dynamic style usage
- passing whole
stylesobjects to components - architectural erosion via unconstrained cross-slice imports (FSD)
The plugin is static, runtime-free, and optimized for ESLint Flat Config (ESLint v9+).
Features
✅ rnzeus/styles-usage
Ensures that all styles declared in *.styles.ts(x) files are actually used in the corresponding component file.
What it checks:
- detects unused style keys
- forbids passing the whole
stylesobject (styles={styles}) - supports arrays:
style={[styles.foo, condition && styles.bar]} - supports conditional styles
- supports dynamic styles only with explicit directive comments
- supports additional “style-like” props (configurable): e.g.
contentContainerStyle
File convention (required):
Component.tsx
Component.styles.ts✅ rnzeus/styles-naming
Enforces snake_case naming for style keys.
Example:
// ❌ bad
itemName: {
}
// ✅ good
item_name: {
}✅ rnzeus/slice-imports
Enforces FSD import boundaries for alias imports:
- layer order: lower layers cannot import higher layers
- same-slice imports must be relative
- cross-slice imports inside the same layer are allowed only via
@x/<currentSlice>
This rule validates only alias imports starting with @ (relative imports are ignored).
Assumed structure (defaults):
srcRoot:"src"layers:shared -> entities -> features -> widgets -> pages -> app
Examples (same slice must be relative):
// file: src/features/auth/ui/Login.tsx
// ❌ bad (same slice via alias)
import { submit } from "@features/auth/model/submit";
// ✅ good
import { submit } from "../model/submit";Examples (cross-slice via @x):
// file: src/entities/product/ui/ProductCard.tsx
// ❌ bad (direct cross-slice import inside same layer)
import { getCartTotal } from "@entities/cart/model/getCartTotal";
// ✅ good (cart exposes cross-slice API specifically for product slice)
import { getCartTotal } from "@entities/cart/@x/product";Installation
npm install -D @rnzeus/eslint-plugin
# or
yarn add -D @rnzeus/eslint-pluginPeer dependency:
eslint >= 9
Usage (ESLint Flat Config)
This plugin exports ready-to-use flat config presets.
No .default access and no manual plugin wiring is required.
Import presets
const styles = require("@rnzeus/eslint-plugin/configs/styles");
const imports = require("@rnzeus/eslint-plugin/configs/imports");Minimal setup
module.exports = [...styles, ...imports];With other presets
const rnfsd = require("@rnzeus/themis/eslint/rnfsd");
const base = require("@rnzeus/eslint-plugin/configs/base");
const styles = require("@rnzeus/eslint-plugin/configs/styles");
const imports = require("@rnzeus/eslint-plugin/configs/imports");
module.exports = [...rnfsd, ...styles, ...imports];Configure styles-usage style props
By default, rnzeus/styles-usage tracks style usage from these JSX props:
stylecontentContainerStyle
You can extend or override this list via rule options.
Merge (default)
type: "merge" is the default behavior:
- takes plugin defaults
- adds your
styleProps - de-duplicates
Example (add FlatList prop):
module.exports = [
...styles,
{
rules: {
"rnzeus/styles-usage": [
"error",
{
styleProps: ["columnWrapperStyle"],
type: "merge", // optional (default)
},
],
},
},
];Override
type: "override" ignores plugin defaults and uses only your styleProps.
module.exports = [
...styles,
{
rules: {
"rnzeus/styles-usage": [
"error",
{
styleProps: ["style"],
type: "override",
},
],
},
},
];Dynamic styles (important)
Dynamic access like this is not allowed by default:
<View style={styles[variant]} />You must explicitly document allowed keys using a directive comment:
// rnzeus-styles-used: primary | secondary
<View style={styles[variant]} />Supported comment forms:
// rnzeus-styles-used: a | b
/* rnzeus-styles-used: a | b */This makes dynamic styles:
- explicit
- reviewable
- safe during refactors
Configure slice-imports
rnzeus/slice-imports supports these options:
srcRoot(default:"src"): marker folder used to detect layer/slice from absolute filenamelayers(default:["shared","entities","features","widgets","pages","app"]): allowed layers in dependency order
Example (custom srcRoot and layers):
const imports = require("@rnzeus/eslint-plugin/configs/imports");
module.exports = [
...imports,
{
rules: {
"rnzeus/slice-imports": [
"error",
{
srcRoot: "src",
layers: ["shared", "entities", "features", "widgets", "pages", "app"],
},
],
},
},
];Rules
rnzeus/styles-usage
| Check | Status |
| --------------------------------------- | ----------------------- |
| Unused styles | ✅ |
| Whole styles object (styles={styles}) | ❌ |
| Array styles | ✅ |
| Conditional styles | ✅ |
| Dynamic styles | ⚠️ (directive required) |
| Extra style props | ✅ (configurable) |
rnzeus/styles-naming
| Rule | Status | | --------------- | ------ | | snake_case only | ✅ | | camelCase | ❌ | | PascalCase | ❌ |
Regex used:
/^[a-z][a-z0-9_]*$/;rnzeus/slice-imports
| Check | Status |
| ---------------------------------------- | ------- |
| Layer order (lower cannot import higher) | ✅ |
| Same slice must be relative | ✅ |
| Cross-slice via @x/<currentSlice> | ✅ |
| Relative imports | Ignored |
Philosophy
This plugin intentionally:
- avoids runtime helpers
- avoids TypeScript compiler APIs
- avoids project-wide analysis
- favors strict conventions over heuristics
The goal is predictability, performance, and refactor safety.
License
MIT
