npm package: @rnzeus/eslint-plugin
Description: ESLint plugin for React / React Native projects focused on **style discipline** and **FSD import boundaries**.
Latest version: 0.1.5

# @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.ts` files
- inconsistent style naming
- unsafe dynamic style usage
- passing whole `styles` objects 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 `styles` object (`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:**

```ts
// ❌ 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):**

```ts
// 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):**

```ts
// 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

```bash
npm install -D @rnzeus/eslint-plugin
# or
yarn add -D @rnzeus/eslint-plugin
```

> Peer 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

```js
const styles = require("@rnzeus/eslint-plugin/configs/styles");
const imports = require("@rnzeus/eslint-plugin/configs/imports");
```

### Minimal setup

```js
module.exports = [...styles, ...imports];
```

### With other presets

```js
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:

- `style`
- `contentContainerStyle`

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):

```js
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`.

```js
module.exports = [
  ...styles,
  {
    rules: {
      "rnzeus/styles-usage": [
        "error",
        {
          styleProps: ["style"],
          type: "override",
        },
      ],
    },
  },
];
```

---

## Dynamic styles (important)

Dynamic access like this is **not allowed by default**:

```tsx
<View style={styles[variant]} />
```

You **must** explicitly document allowed keys using a directive comment:

```tsx
// rnzeus-styles-used: primary | secondary
<View style={styles[variant]} />
```

Supported comment forms:

```ts
// 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 filename
- `layers` (default: `["shared","entities","features","widgets","pages","app"]`): allowed layers in dependency order

Example (custom `srcRoot` and layers):

```js
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:

```ts
/^[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