npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

eslint-plugin-clean-fsd

v0.2.0

Published

ESLint plugin for Clean FSD — F.S.D layered on CQRS (read/write) and DDD (single-Aggregate/multi-Aggregate) axes

Readme

eslint-plugin-clean-fsd

ESLint plugin for Clean FSD — Feature-Sliced Design layered on two axes:

  1. CQRS axis — read vs. write
  2. DDD axis — single-Aggregate internal vs. multi-Aggregate coordination

Built for Next.js + Supabase projects, but the rules apply to any FSD codebase.

Companion article: F.S.D 모호함, CQRS와 DDD 두 축으로 구분합니다

GitHub: https://github.com/boaz-hwang/eslint-plugin-clean-fsd


Why two axes?

Classic FSD gives you layers (entities, features, widgets, ...) but leaves the boundary between entities and features fuzzy. Teams end up debating every PR: "should addItemToCart live in entities/cart or features/add-to-cart?"

Clean FSD resolves that by asking two questions about every piece of logic:

| | Read (Query) | Write (Command) | | ------------------------ | ------------------------------- | ------------------------------- | | Single Aggregate | entities/<slice>/selectors/ | entities/<slice>/commands/ | | Multi-Aggregate | features/<slice>/queries/ | features/<slice>/commands/ |

Once you map a piece of logic onto this 2×2, its location is mechanical — no more PR debates.

In addition, action/ (or api/) folders remain the canonical place for server-boundary code (Next.js Server Actions with 'use server'). This plugin recognizes both.


Installation

npm install -D eslint-plugin-clean-fsd
# or
pnpm add -D eslint-plugin-clean-fsd

Usage

Recommended Config

// eslint.config.mjs
import cleanFsd from "eslint-plugin-clean-fsd";

export default [
  cleanFsd.configs.recommended,
  // ...your other configs
];

Manual Config

// eslint.config.mjs
import cleanFsd from "eslint-plugin-clean-fsd";

export default [
  {
    plugins: { "clean-fsd": cleanFsd },
    rules: {
      "clean-fsd/require-use-server-in-actions": "error",
      "clean-fsd/no-mutation-in-entities": "error",
      "clean-fsd/no-cross-slice-import": "error",
      "clean-fsd/no-upward-entity-import": "error",
      "clean-fsd/no-use-client-in-entities": "warn",
      "clean-fsd/entities-read-only-actions": "warn",
      "clean-fsd/features-write-only-actions": "warn",
      "clean-fsd/no-supabase-in-ui": "warn",
      "clean-fsd/require-public-api": ["warn", { ignoreLayers: ["shared", "app"] }],
    },
  },
];

Folder Conventions

| Folder | Purpose | Read/Write | Aggregate Scope | | ------------------- | ----------------------------------------------------------------------------- | ---------- | ------------------------ | | action/ or api/ | Next.js Server Actions ('use server') — server boundary | Either | Either | | selectors/ | Pure read functions on a single Aggregate (derived state, memos) | Read | Single Aggregate | | commands/ | Write functions that mutate memory state while upholding Aggregate invariants | Write | Single or Multi | | queries/ | Reads that combine multiple Aggregates | Read | Multi-Aggregate |

Plural form is canonical. Singular (selector/, command/, query/) is also accepted for flexibility.

Note: entities/*/commands/ mutates in-memory state only (e.g. cart.changeQuantity()). Supabase mutations (.insert(), .update(), .delete(), .upsert()) remain disallowed anywhere in entities/ — they belong in features/*/action/ or features/*/commands/.


Example Structure (Cart domain)

src/
  entities/cart/
    model/                  # Cart Aggregate Root, CartItem types, invariants
    selectors/              # [single × read]  totalPrice, itemCount, canAdd
    commands/               # [single × write] addItem, changeQuantity, clear
    action/                 # Server Actions:  'use server'; get/load/fetch only
    ui/                     # Server Components by default
    index.ts                # Public API

  features/checkout/
    model/
    queries/                # [multi × read]   cartWithCoupons, cartWithShipping
    commands/               # [multi × write]  completeCheckout (Cart + Order + Stock)
    action/                 # Server Actions:  'use server'; create/update/...
    ui/                     # 'use client' OK
    index.ts

  features/cart-merge/
    commands/               # [multi × write]  mergeGuestAndServerCart (User + Cart)

  widgets/cart-panel/
    ui/
    index.ts

  shared/
    ui/
    lib/
    config/

Rules

Error Rules

| Rule | Description | Fixable | | ----------------------------------------------------------- | ------------------------------------------------------------------------------- | ------- | | require-use-server-in-actions | action/ and api/ files must start with 'use server' | ✅ | | no-mutation-in-entities | The entities layer cannot call Supabase mutation methods | - | | no-cross-slice-import | A slice cannot import another slice in the same layer | - | | no-upward-entity-import | entities cannot import from features, widgets, or app (layer direction) | - |

Warning Rules

These flag patterns that usually indicate architectural drift but sometimes have legitimate exceptions.

| Rule | Description | | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | | no-use-client-in-entities | Warn on 'use client' inside entities/ui/ | | entities-read-only-actions | Exports in entities/*/action/, entities/*/api/, and entities/*/selectors/ should use read prefixes | | features-write-only-actions | Exports in features/*/action/, features/*/api/, and features/*/commands/ should use write prefixes | | no-supabase-in-ui | Warn when @supabase/* is imported directly in ui/ files | | require-public-api | Warn on deep imports into slice internals instead of index.ts |


require-use-server-in-actions

Enforces a top-level 'use server' directive in FSD action files. The rule treats both action/ and api/ as action folders and can auto-fix missing directives.

// Bad: src/entities/user/action/get-user.ts
export async function getUser() { ... }

// Good: src/entities/user/action/get-user.ts
'use server';
export async function getUser() { ... }

no-mutation-in-entities

Disallows Supabase mutation methods such as .insert(), .update(), .delete(), and .upsert() anywhere in the entities layer — including selectors/ and commands/. Memory-state mutations inside commands/ are still allowed; this rule only targets server-side writes.

// Bad: src/entities/user/action/create-user.ts
const { data } = await supabase.from('users').insert({ name });

// Good: move the DB mutation to the features layer
// src/features/user/action/create-user.ts
'use server';
const { data } = await supabase.from('users').insert({ name });

no-cross-slice-import

Disallows importing a different slice from the same layer. Compose those slices from a higher layer such as widgets or app.

// Bad: src/entities/book/ui/BookCard.tsx
import { getPartner } from '@/entities/partner';

// Good: compose them in widgets
// src/widgets/book-with-partner/ui/BookWithPartner.tsx
import { BookCard } from '@/entities/book';
import { PartnerBadge } from '@/entities/partner';

no-upward-entity-import

Disallows entities/* files from importing features/*, widgets/*, or app/*. Dependencies must flow downward in the FSD layer graph.

// Bad: src/entities/cart/commands/add-item.ts
import { startCheckout } from '@/features/checkout';

// Good: coordination that needs `features` belongs in `features`
// src/features/cart-merge/commands/merge.ts
import { loadCart } from '@/entities/cart';
import { getCurrentUser } from '@/entities/user';

no-use-client-in-entities

Warns when an entities/ui file starts with 'use client'. Entity UI should stay server-first unless there is a clear reason to make it client-side.

// Warning: src/entities/user/ui/UserList.tsx
'use client';

// Preferred: Server Component
export async function UserList() {
  const users = await getUsers();
  return <ul>{users.map((user) => <li key={user.id}>{user.name}</li>)}</ul>;
}

entities-read-only-actions

Checks that exported functions in entities/*/action/, entities/*/api/, and entities/*/selectors/ start with a read-oriented prefix:

get, fetch, load, search, count, exists, subscribe, find, list, check, query

// Warning: src/entities/user/action/create-user.ts
export async function createUser() { ... } // This belongs in features/

// Good: src/entities/user/action/get-user.ts
export async function getUser() { ... }

// Good: src/entities/cart/selectors/total.ts
export function getTotal(cart: Cart) { ... }

features-write-only-actions

Checks that exported functions in features/*/action/, features/*/api/, and features/*/commands/ start with a write-oriented prefix:

create, update, delete, submit, toggle, archive, restore, batch, remove, add, upsert, patch, put, post

// Warning: src/features/user/action/get-user.ts
export async function getUser() { ... } // This belongs in entities/

// Good: src/features/user/action/create-user.ts
export async function createUser() { ... }

// Good: src/features/checkout/commands/complete-checkout.ts
export async function completeCheckout(...) { ... }

features/*/queries/ is not checked by this rule, because queries are reads. Use read-oriented prefixes there.

no-supabase-in-ui

Warns when a file in a ui segment imports @supabase/* directly. Keep database access in server actions instead of UI modules.

// Warning: src/entities/user/ui/UserList.tsx
import { createClient } from '@supabase/supabase-js';

// Good: import through an action file
// src/entities/user/action/get-users.ts
import { createClient } from '@supabase/supabase-js';

require-public-api

Warns on deep alias imports into FSD slice internals. Import through the slice public API instead.

// Warning
import { UserForm } from '@/features/user/ui/UserForm';

// Good
import { UserForm } from '@/features/user';

Options

  • ignoreLayers: layers to skip when checking deep imports. Default: ["shared", "app"]

Conventions Recognized by the Plugin

  • FSD layers: app, widgets, features, entities, shared
  • Server-boundary folders: action, api
  • Aggregate internal read folders: selectors (also selector)
  • Aggregate internal write folders: commands (also command)
  • Multi-Aggregate read folders: queries (also query)
  • Public API import pattern: @/layer/slice

Migrating from v0.1.x

v0.2.0 is fully backward compatible. Existing action/ and api/ conventions continue to work unchanged.

Opt-in changes you may want to make:

  • Move pure single-Aggregate reads into selectors/
  • Move single-Aggregate writes (memory state, invariant maintenance) into commands/
  • Move multi-Aggregate reads into features/*/queries/
  • Enable the new no-upward-entity-import rule (included in recommended)

No rules have been removed or had their severity raised.


Compatibility

  • ESLint >= 9.0.0
  • Node.js >= 18.0.0

License

MIT