eslint-plugin-fsd-lint
Version:
ESLint plugin for enforcing Feature-Sliced Design (FSD) architecture
630 lines (461 loc) ⢠20.4 kB
Markdown
# š eslint-plugin-fsd-lint š
[](https://www.npmjs.com/package/eslint-plugin-fsd-lint)
[](https://www.npmjs.com/package/eslint-plugin-fsd-lint)
[](https://bundlephobia.com/package/eslint-plugin-fsd-lint)
[](https://github.com/effozen/eslint-plugin-fsd-lint/blob/main/LICENSE)
[English](README.md) | [ķźµģ“](README.ko.md)
> It supports ESLint 9+ and the new Flat Config system.
## š Introduction
`eslint-plugin-fsd-lint` is an ESLint plugin that enforces best practices for Feature-Sliced Design (FSD) architecture. <br/>
It is fully **compatible with ESLint 9+** and follows the modern **Flat Config format**, ensuring seamless integration into modern JavaScript and TypeScript projects.
### ⨠Why use this plugin?
- **Flat Config support**: Fully compatible with **ESLint 9+** and the **new Flat Config system**.
- **Strict FSD compliance**: Prevents architectural violations in feature-based project structures.
- **Improves maintainability**: Encourages clear module separation and dependency control.
- **Ensures consistent code quality**: Standardizes import patterns and best practices.
- **Cross-platform compatibility**: Works seamlessly on both Windows and Unix-based systems.
- **Flexible folder naming**: Supports custom folder naming patterns (e.g., `1_app`, `2_pages`).
- **Multiple alias formats**: Supports both `@shared` and `@/shared` import styles.
- **Comprehensive test coverage**: Thoroughly tested with real-world scenarios and edge cases.
### š What is Feature-Sliced Design?
Feature-Sliced Design (FSD) is a modern architecture pattern that provides a structured approach to organizing frontend applications. <br/>
This plugin enforces key FSD principles such as **proper layer separation, import restrictions, and dependency management**,
helping developers build scalable and maintainable codebases.
---
## š¦ Installation
You can install `eslint-plugin-fsd-lint` via **npm** or **pnpm**:
### Using npm:
```shell
npm install --save-dev eslint-plugin-fsd-lint
```
### Using pnpm:
```shell
pnpm add -D eslint-plugin-fsd-lint
```
### Peer Dependencies
This plugin requires ESLint 9+ to work properly.
Make sure you have ESLint installed in your project:
```shell
npm install --save-dev eslint
```
> š” Tip: If you're using a monorepo with multiple packages, install eslint-plugin-fsd-lint at the root level to share the configuration across all workspaces.
---
## š Usage & Configuration
### š§ Flat Config Setup (`eslint.config.mjs`)
`eslint-plugin-fsd-lint` is designed for **ESLint 9+** and works seamlessly with the **Flat Config system**. <br/>
To use it in your project, add the following configuration to your `eslint.config.mjs`:
```js
import fsdPlugin from 'eslint-plugin-fsd-lint';
export default [
// Use the recommended preset
fsdPlugin.configs.recommended,
// Or configure rules individually
{
plugins: {
fsd: fsdPlugin,
},
rules: {
// Enforces FSD layer import rules (e.g., features cannot import pages)
'fsd/forbidden-imports': 'error',
// Disallows relative imports between slices/layers, use aliases (@)
// Allows relative imports within the same slice by default (configurable)
'fsd/no-relative-imports': 'error',
// Enforces importing only via public API (index files)
'fsd/no-public-api-sidestep': 'error',
// Prevents direct imports between slices in the same layer
'fsd/no-cross-slice-dependency': 'error',
// Prevents UI imports in business logic layers (e.g., entities)
'fsd/no-ui-in-business-logic': 'error',
// Forbids direct import of the global store
'fsd/no-global-store-imports': 'error',
// Enforces import order based on FSD layers
'fsd/ordered-imports': 'warn',
},
},
];
```
**Key Principles Enforced by Default:**
- **Layered Imports**: Higher layers cannot import from lower layers (e.g., `features` cannot import `pages`).
- **Slice Isolation**: Slices within the same layer should not directly import each other (`no-cross-slice-dependency`).
- **Public API Usage**: Imports from other slices/layers must go through their public API (`index.js` or `index.ts`). Direct internal imports are forbidden (`no-public-api-sidestep`).
- **Absolute Paths (Aliases)**: Use absolute paths (configured via aliases like `@`) for imports between different slices or layers. Relative paths are generally disallowed (`no-relative-imports`).
- **Relative Paths within Slices**: Relative paths are **allowed** for imports _within the same slice_ (e.g., importing a helper from `../lib` inside a feature). This is configurable via the `allowSameSlice` option in `no-relative-imports` (default: true).
### š Available Configurations
The plugin provides three pre-defined configurations for different strictness levels:
```js
import fsdPlugin from 'eslint-plugin-fsd-lint';
export default [
// Standard recommended configuration
fsdPlugin.configs.recommended,
// Strict configuration (all rules as error)
// fsdPlugin.configs.strict,
// Base configuration (less strict)
// fsdPlugin.configs.base,
];
```
### š ļø Advanced Configuration
You can customize the behavior of the rules with advanced options:
```js
import fsdPlugin from 'eslint-plugin-fsd-lint';
export default [
{
plugins: {
fsd: fsdPlugin,
},
rules: {
// Configure alias format and folder patterns
'fsd/forbidden-imports': [
'error',
{
// Support for @shared or @/shared import styles
alias: {
value: '@',
withSlash: false, // Use true for @/shared format
},
// Support for numbered folder prefixes
folderPattern: {
enabled: true,
regex: '^(\\d+_)?(.*)',
extractionGroup: 2,
},
},
],
// Other rules...
},
},
];
```
### š Example Project Structure
Here's how an FSD-compliant project might look:
```plaintext
src/
āāā app/ (or 1_app/)
ā āāā providers/
ā āāā store.js
ā āāā index.js // Public API for app
ā
āāā processes/ (or 2_processes/)
ā āāā auth/
ā āāā onboarding/
ā
āāā pages/ (or 3_pages/)
ā āāā HomePage/
ā ā āāā ui/ // UI components for HomePage
ā ā āāā index.ts // Public API for HomePage
ā āāā ProfilePage/
ā
āāā widgets/ (or 4_widgets/)
ā āāā Navbar/
ā ā āāā ui/
ā ā āāā index.ts // Public API for Navbar
ā āāā Sidebar/
ā
āāā features/ (or 5_features/)
ā āāā login/
ā ā āāā ui/ // UI components for login feature
ā ā āāā model/ // Business logic for login
ā ā āāā index.ts // Public API for login feature
ā āāā registration/
ā
āāā entities/ (or 6_entities/)
ā āāā user/
ā ā āāā ui/
ā ā āāā model/
ā ā āāā index.ts // Public API for user entity
ā āāā post/
ā
āāā shared/ (or 7_shared/)
ā āāā ui/ // Reusable UI components
ā ā āāā Button/ // Example: Button component
ā āāā lib/ // Utility functions, helpers
ā āāā config/ // Shared configuration
ā āāā index.ts // Public API for shared layer (optional)
```
**Import Examples based on Structure:**
- **Allowed (Alias Import - Cross Slice/Layer):**
```javascript
// features/login/ui/LoginForm.tsx
import { Button } from '@shared/ui/Button'; // OK: Feature uses Shared UI via alias
import { getUser } from '@entities/user'; // OK: Feature uses User entity via public API
```
- **Allowed (Relative Import - Same Slice):**
```javascript
// features/login/ui/LoginForm.tsx
import { validateInput } from '../lib/validation'; // OK: Relative import within the 'login' feature slice
```
- **Disallowed (Relative Import - Cross Slice/Layer):**
```javascript
// features/login/ui/LoginForm.tsx
import { config } from '../../../app/config'; // BAD: Relative path to different layer
import { User } from '../../entities/user/model/types'; // BAD: Relative path + Public API sidestep
```
- **Disallowed (Public API Sidestep):**
```javascript
// features/login/ui/LoginForm.tsx
import { userSlice } from '@entities/user/model/slice'; // BAD: Importing internal module directly
```
- **Disallowed (Cross-Slice Dependency):**
```javascript
// features/login/model/auth.ts
import { startRegistration } from '@features/registration'; // BAD: 'login' feature directly imports 'registration' feature
```
> š” Tip: Sticking to these import rules, especially using aliases and respecting public APIs, makes your codebase much easier to refactor and maintain.
---
## š Supported Rules
This plugin provides a set of ESLint rules that enforce **Feature-Sliced Design (FSD) best practices**. <br/>
Each rule helps maintain a **clear module structure, enforce import constraints, and prevent architectural violations**.
| Rule | Description |
| --------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| **fsd/forbidden-imports** | Prevents imports from higher layers and cross-imports between slices. |
| **fsd/no-relative-imports** | Disallows relative imports across different slices or layers. Allows relative imports within the same slice. |
| **fsd/no-public-api-sidestep** | Prevents direct imports from internal modules, enforcing public API usage. |
| **fsd/no-cross-slice-dependency** | Disallows direct dependencies between slices in the same layer (applies to all layers, not just features). |
| **fsd/no-ui-in-business-logic** | Prevents UI imports inside business logic layers (e.g., `entities`). |
| **fsd/no-global-store-imports** | Forbids direct imports of global state (`store`). |
| **fsd/ordered-imports** | Enforces import grouping by layer. |
---
## š Rule Details & Examples
### **1ļøā£ fsd/forbidden-imports**
**Prevents imports from higher layers and cross-imports between slices.** <br/>
ā
**Allowed:** `features` can import from `entities` or `shared` <br/>
ā **Not Allowed:** `features` importing directly from `app`<br/>
```js
// ā Incorrect (feature importing from app)
import { config } from '../../app/config';
// ā
Correct (feature importing from entities/shared)
import { getUser } from '../../entities/user';
import { formatCurrency } from '../../shared/utils';
```
<br/>
### 2ļøā£ fsd/no-relative-imports
Disallows relative imports across different slices or layers. <br/>
ā
Allowed: Using project-defined aliases or relative imports within same slice <br/>
ā Not Allowed: Relative imports between different slices <br/>
```javascript
// ā Incorrect (relative import across different slices)
import { fetchUser } from '../another-slice/model/api';
// ā
Correct (relative import within the same slice)
import { fetchData } from '../model/api';
// ā
Correct (alias import across slices or layers)
import { Button } from '@shared/ui/Button';
// Also supports @/shared format
import { Button } from '@/shared/ui/Button';
```
#### Available options:
```javascript
// In your eslint.config.js:
'fsd/no-relative-imports': ['error', {
// Allow relative imports within the same slice (enabled by default)
allowSameSlice: true,
// Allow type-only imports to use relative paths (disabled by default)
allowTypeImports: false,
// Patterns for test files that can ignore this rule
testFilesPatterns: ['\\.test\\.', '\\.spec\\.'],
// Patterns for imports to ignore
ignoreImportPatterns: []
}]
```
<br/>
### 3ļøā£ fsd/no-public-api-sidestep
Prevents direct imports from internal modules of features, widgets, or entities. <br/>
ā
Allowed: Importing from index.ts (public API) or segment level <br/>
ā Not Allowed: Importing internal files within segments <br/>
```javascript
// ā
Correct (importing via public API)
import { authSlice } from '../../features/auth';
import { userModel } from '@entities/user/model'; // Segment level is allowed
// ā Incorrect (direct internal import)
import { authSlice } from '../../features/auth/slice.ts';
import { userSlice } from '@entities/user/model/slice'; // Internal file not allowed
```
**Note:** Any segment name is allowed (not limited to model/ui/api/lib). This provides flexibility for teams to define their own segment structure.
<br/>
### 4ļøā£ fsd/no-cross-slice-dependency
Prevents direct dependencies between slices in the same layer (applies to all layers, not just features). <br/>
ā
Allowed: Communication via lower layers <br/>
ā Not Allowed: Direct imports between different slices in the same layer <br/>
```javascript
// ā Incorrect (slice importing from another slice in the same layer)
import { processPayment } from '../../features/payment';
// ā
Correct (using entities/shared as an intermediary)
import { PaymentEntity } from '../../entities/payment';
// ā Also incorrect (entities slice importing from another entities slice)
import { Product } from '../../entities/product';
// This rule now applies to all layers, not just features!
```
<br/>
### 5ļøā£ fsd/no-ui-in-business-logic
Prevents UI imports inside business logic layers (e.g., entities). <br/>
ā
Allowed: UI should only be used inside widgets or pages <br/>
ā Not Allowed: entities importing UI components <br/>
```javascript
// ā Incorrect (entity importing widget)
import { ProfileCard } from '../../widgets/ProfileCard';
// ā
Correct (widget using entity data)
import { getUser } from '../../entities/user';
```
<br/>
### 6ļøā£ fsd/no-global-store-imports
Forbids direct imports of global state (store). <br/>
ā
Allowed: Using useStore or useSelector <br/>
ā Not Allowed: Direct imports of the store <br/>
```javascript
// ā Incorrect (direct import of store)
import { store } from '../../app/store';
// ā
Correct (using hooks)
import { useStore } from 'zustand';
import { useSelector } from 'react-redux';
```
<br/>
### 7ļøā£ fsd/ordered-imports
Enforces import grouping by layer. <br/>
ā
Allowed: Grouping imports by layer <br/>
ā Not Allowed: Mixed import order <br/>
```javascript
// ā Incorrect (random import order)
import { processPayment } from '../features/payment';
import { getUser } from '../entities/user';
import { formatCurrency } from '../shared/utils';
import { loginUser } from '../features/auth';
import { Header } from '../widgets/Header';
import { useStore } from '../app/store';
// ā
Correct (layered grouping)
import { useStore } from '../app/store'; // App
import { loginUser } from '../features/auth'; // Features
import { processPayment } from '../features/payment';
import { getUser } from '../entities/user'; // Entities
import { formatCurrency } from '../shared/utils'; // Shared
import { Header } from '../widgets/Header'; // Widgets
```
> š” Tip: Use `npx eslint --fix` to automatically reorder imports according to FSD layers.
---
## š Auto-fix Support
Certain rules in `eslint-plugin-fsd-lint` support **automatic fixing** using ESLint's `--fix` option. <br/>
This allows developers to quickly resolve violations **without manual code adjustments**.
### ā
Rules Supporting Auto-fix
The following rules can be automatically fixed:
| Rule | Description |
| ----------------------- | ------------------------------------------------------------------------ |
| **fsd/ordered-imports** | Automatically sorts imports based on Feature-Sliced Design (FSD) layers. |
### š§ Using `--fix` in ESLint
To apply automatic fixes to your project, simply run:
```shell
npx eslint --fix your-file.js
```
Or, to fix all files in your project:
```shell
npx eslint --fix .
```
### š Example Before & After Auto-fix
ā Before (fsd/ordered-imports violation)
```javascript
import { processPayment } from '../features/payment';
import { getUser } from '../entities/user';
import { formatCurrency } from '../shared/utils';
import { loginUser } from '../features/auth';
import { Header } from '../widgets/Header';
import { useStore } from '../app/store';
```
<br/>
ā
After (npx eslint --fix applied)
```javascript
import { useStore } from '../app/store'; // App
import { loginUser } from '../features/auth'; // Features
import { processPayment } from '../features/payment';
import { getUser } from '../entities/user'; // Entities
import { formatCurrency } from '../shared/utils'; // Shared
import { Header } from '../widgets/Header'; // Widgets
```
> š” Tip: `fsd/ordered-imports` ensures a clean and structured import order based on FSD layers.
---
## š New Features
### 1. Cross-Platform Compatibility
The plugin now works seamlessly on both Windows and Unix-based systems by normalizing file paths internally.
### 2. Flexible Folder Naming Patterns
You can now use numbered prefixes or other naming conventions for your folders:
```js
// Configure folder pattern support
"fsd/forbidden-imports": ["error", {
folderPattern: {
enabled: true,
regex: "^(\\d+_)?(.*)",
extractionGroup: 2
}
}]
```
This allows using structures like:
```
src/
1_app/
2_pages/
3_widgets/
4_features/
5_entities/
6_shared/
```
### 3. Multiple Alias Format Support
The plugin now supports both `@shared` and `@/shared` import styles:
```js
// Configure alias format
"fsd/forbidden-imports": ["error", {
alias: {
value: "@",
withSlash: false // Use true for @/shared format
}
}]
```
### 4. Enhanced Cross-Slice Dependency Rule
The `no-cross-slice-dependency` rule now applies to all layers by default, not just features:
```js
// Restrict to features layer only (legacy behavior)
"fsd/no-cross-slice-dependency": ["error", {
featuresOnly: true
}]
```
### 5. Pre-defined Configuration Profiles
Multiple configuration presets are now available:
- `recommended` - Standard recommended settings
- `strict` - Maximum enforcement
- `base` - Less strict settings for easy adoption
### 6. Comprehensive Test Coverage
The plugin now includes extensive test cases for all rules, covering:
- Basic import scenarios
- Edge cases and complex patterns
- Path variations (Windows, Unix, mixed)
- Custom configurations
- Real-world usage examples
### Path Alias Support
The plugin now properly handles various path alias formats:
```javascript
// Both formats are supported
import { UserCard } from '@entities/user';
import { UserCard } from '@/entities/user';
```
### Dynamic Import Support
All rules now support dynamic imports:
```javascript
// Valid dynamic imports
const UserCard = await import('@entities/user');
const { UserCard } = await import('@entities/user');
// Invalid dynamic imports (will be caught by rules)
const UserCard = await import('@entities/user/ui');
```
### Comprehensive Test Coverage
All rules are now thoroughly tested with:
- Basic import scenarios
- Edge cases and complex patterns
- Path variations (Windows, Unix, mixed)
- Custom configurations
- Real-world usage examples
- Path alias formats
- Dynamic import patterns
---
## š¤ Contributing
We welcome contributions to improve `eslint-plugin-fsd-lint`! <br/>
If you have an idea for a new rule or an improvement, feel free to submit a Pull Request.
Check out our [contribution guide](CONTRIBUTING.md).
---
## š License
This project is licensed under the MIT License. <br/>
See the [LICENSE](LICENSE.md) file for details.
<br/>