eslint-plugin-fsd-boundaries
v1.1.0
Published
ESLint plugin that enforces Feature-Sliced Design boundaries: Public API imports, layer direction, and relative-vs-absolute import paths — with autofixes.
Maintainers
Readme
eslint-plugin-fsd-boundaries
ESLint plugin that enforces Feature-Sliced Design import boundaries — with autofixes.
It ships three focused rules that keep imports honest in an FSD codebase:
| Rule | What it enforces | Autofix |
| --- | --- | --- |
| public-api | Slices may only be imported through their Public API; shared/app imports must target a segment | ✅ |
| layer-imports | A layer may only import lower layers; one slice may not import a sibling slice of the same layer | — |
| relative-path | Imports inside a module must be relative; imports into another module must be absolute | ✅ (both directions) |
The FSD layers
Imports are validated against the standard layer hierarchy (low → high):
shared → entities → features → widgets → pages → appA layer may import from the layers below it, never above. shared and app have no slices — they are organized by segment (shared/ui, shared/lib, app/providers, …).
Installation
npm install --save-dev eslint-plugin-fsd-boundaries
# or
yarn add -D eslint-plugin-fsd-boundariesRequires ESLint >=9.15 (flat config).
Usage
Recommended config
// eslint.config.js
import fsdBoundaries from "eslint-plugin-fsd-boundaries";
export default [
fsdBoundaries.configs.recommended,
];This enables all three rules as errors with the default alias @.
Manual config
// eslint.config.js
import fsdBoundaries from "eslint-plugin-fsd-boundaries";
export default [
{
plugins: { "fsd-boundaries": fsdBoundaries },
rules: {
"fsd-boundaries/public-api": "error",
"fsd-boundaries/layer-imports": "error",
"fsd-boundaries/relative-path": ["error", { alias: "@" }],
},
},
];Alias configuration
Every rule accepts an alias option (default "@") used to recognize absolute FSD imports:
"fsd-boundaries/public-api": ["error", { alias: "~" }] // matches "~/entities/users"Set alias to an empty string if your project uses bare layer-rooted imports:
"fsd-boundaries/public-api": ["error", { alias: "" }] // matches "entities/users"Imports whose first segment is not an FSD layer (e.g. react, @scope/pkg) are treated as library imports and ignored.
Rules
public-api
Forbids reaching past a slice's Public API.
// ❌ deep import bypasses the slice barrel
import { mapUser } from "@/entities/users/model/mapUser";
// ✅ autofixed to
import { mapUser } from "@/entities/users";
// shared / app are segment-based: a segment is required
import x from "@/shared"; // ❌ bare layer — error (no autofix)
import { Button } from "@/shared/ui"; // ✅
import { Button } from "@/shared/ui/Button/Button"; // ✅ deeper is allowedImports that point inside the current file's own module are left to relative-path (no conflict).
Options: { alias?: string, exceptions?: string[] }. Paths whose clean form starts with any exceptions prefix are skipped (e.g. exceptions: ["entities/users/testing"]).
layer-imports
Two checks, both skipping type-only imports (import type … and inline import { type Foo }):
// In src/entities/users/ui/Card.tsx:
import { Foo } from "@/features/foo"; // ❌ entities cannot import the higher layer features
// In src/features/foo/ui/Foo.tsx:
import { Bar } from "@/features/bar"; // ❌ a feature cannot import a sibling feature (cross-slice)
import type { Bar } from "@/features/bar"; // ✅ type-only is allowedCross-slice is only checked for sliced layers; cross-segment imports inside shared/app are fine.
Options: { alias?: string, exceptions?: string[] }.
exceptions is a list of layer-rooted path prefixes allowed to bypass both checks — handy for the classic "type the store in a lower layer" case:
"fsd-boundaries/layer-imports": ["error", {
exceptions: ["app/providers/StoreProvider"],
}](If the import is type-only — import type { StateSchema } from "@/app/providers/StoreProvider" — it is already allowed without an exception.)
relative-path
Keeps import style consistent with module boundaries.
// In src/features/foo/ui/Foo.tsx (module = features/foo):
import { x } from "@/features/foo/model/x"; // ❌ same module → must be relative
import { x } from "../model/x"; // ✅ (autofix target)
import { Bar } from "../../bar/ui/Bar"; // ❌ different module → must be absolute
import { Bar } from "@/features/bar/ui/Bar"; // ✅ (autofix target)A "module" is layer/slice for sliced layers, and layer/segment/unit (e.g. shared/ui/Card) for shared/app. Path math is computed symbolically, so it works identically on Windows and POSIX.
Options: { alias?: string }.
License
MIT
