Feature-Sliced Design
Markdown for agentsThis guide checks a front end that uses Feature-Sliced Design
(FSD) with the fsd preset. The preset holds the rules of Steiger, the official linter for FSD.
Before you start
Section titled “Before you start”- Read Getting started.
- Your front end has the FSD layers in one folder:
app,pages,widgets,features,entitiesandshared. A repository needs only some of them.
The example is a small store front. An import between two slices uses the alias @/, which
tsconfig.json points at src:
- archangel.config.ts
- tsconfig.json
Directorysrc/
Directoryapp/
- index.ts
Directorypages/
Directoryhome/
- index.ts
Directoryui/
- home-page.ts
Directoryfeatures/
Directoryadd-to-cart/
- index.ts
Directorymodel/
- cart.ts
Directoryui/
- add-to-cart-button.ts
Directoryentities/
Directoryproduct/
- index.ts
Directorymodel/
- product.ts
Directoryui/
- product-card.ts
Directoryshared/
Directoryui/
- button.ts
- index.ts
Add the preset
Section titled “Add the preset”-
Put the preset in
extends.rootis the folder that holds the layers:archangel.config.ts import { defineConfig, fsd } from '@enhance-eng/archangel';export default defineConfig({extends: [fsd({ root: 'src' })],});The config has no parts and no rules of its own. The preset declares each layer as a part, and each slice as an instance.
-
Check the repository:
Terminal $ npx archangel checkerror[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.--> src/entities/product= note: rule: fsd/insignificant-slice= note: reason: A slice that at most one other slice uses belongs in that slice.= note: see: https://feature-sliced.github.io/steiger/rules/insignificant-sliceerror[may-not-import]: src/entities/product/ui/product-card.ts may not import "@/features/add-to-cart", because files in "entities" may not import "fsd/features", "fsd/widgets", "fsd/pages", "fsd/app"--> src/entities/product/ui/product-card.ts:1:33|1 | import { addToCartButton } from '@/features/add-to-cart';| ^^^^^^^^^^^^^^^^^^^^^^^^ remove the import, or move the code to files that may import it|= note: rule: fsd/forbidden-imports= note: reason: A module may import only from layers below its own, and a slice not from another slice of its layer, except through @x.= note: see: https://feature-sliced.github.io/steiger/rules/forbidden-importschecked 12 files and 13 imports: 2 problemsfsd/widgets matches nothing in this repositoryfsd/processes matches nothing in this repositoryFor what a code means, run `archangel docs plugin` or `archangel docs may-not-import`.The check finds two problems:
fsd/forbidden-imports: the entityproductimports the featureadd-to-cart. In FSD, a layer imports only from the layers below it.fsd/insignificant-slice: only one other slice uses the entityproduct. Steiger tells you to merge it into that slice.
The summary also lists the preset parts that find no file, here
widgetsandprocesses. This is not an error. A preset declares each layer of FSD, and most repositories have only some of them. -
Run the examples:
Output of
archangel testTerminal $ npx archangel testpass fsd/forbidden-imports accepted #1 src/entities/example-user/index.ts, src/features/example-login/model/a.tspass fsd/forbidden-imports accepted #2 src/entities/example-a/model/a.ts, src/entities/example-b/@x/example-a.tspass fsd/forbidden-imports rejected #1 src/features/example-login/index.ts, src/shared/example-lib/index.tspass fsd/forbidden-imports rejected #2 src/features/example-a/model/a.ts, src/features/example-b/index.tspass fsd/no-public-api-sidestep accepted #1 src/features/example-login/index.ts, src/pages/example-home/ui/a.tspass fsd/no-public-api-sidestep rejected #1 src/features/example-login/model/x.ts, src/pages/example-home/ui/a.tspass fsd/no-public-api-sidestep rejected #2 src/features/example-login/model/a.ts, src/shared/example-api/client.ts, src/shared/example-api/index.tspass fsd/public-api accepted #1 src/features/example-login/index.ts, src/features/example-login/model/x.tspass fsd/public-api rejected #1 src/features/example-login/model/x.tspass fsd/no-layer-public-api rejected #1 src/entities/index.tspass fsd/no-reserved-folder-names rejected #1 src/shared/example-kit/ui/button.tspass fsd/no-segmentless-slices accepted #1 src/features/example-login/index.ts, src/features/example-login/model/login.tspass fsd/no-segmentless-slices rejected #1 src/features/example-login/index.ts, src/features/example-login/login.tspass fsd/no-segments-on-sliced-layers rejected #1 src/features/ui/button.tspass fsd/no-ui-in-app rejected #1 src/app/ui/root.tsxpass fsd/no-ui-in-app rejected #2 src/app/ui.tsxpass fsd/segments-by-purpose rejected #1 src/features/example-login/utils/format.tspass fsd/segments-by-purpose rejected #2 src/shared/helpers.tspass fsd/no-processes rejected #1 src/processes/example-checkout/index.tspass fsd/typo-in-layer-name accepted #1 src/features/example-login/index.tspass fsd/typo-in-layer-name rejected #1 src/featurez/example.tspass fsd/ambiguous-slice-names accepted #1 src/features/example-login/model/a.tspass fsd/ambiguous-slice-names rejected #1 src/features/example-kit/model/a.ts, src/shared/example-kit/index.tspass fsd/inconsistent-naming accepted #1 src/entities/example-group/apples/model/a.ts, src/entities/example-group/pears/model/a.tspass fsd/inconsistent-naming rejected #1 src/entities/example-group/order-line/model/a.ts, src/entities/example-group/orders/model/a.tspass fsd/insignificant-slice accepted #1 src/entities/example-used/index.ts, src/entities/example-used/model/a.ts, src/pages/example-one/ui/a.ts, src/pages/example-two/ui/b.tspass fsd/insignificant-slice rejected #1 src/entities/example-lonely/index.ts, src/entities/example-lonely/model/a.tspass fsd/repetitive-naming accepted #1 src/features/example-group/drop/model/a.ts, src/features/example-group/load/model/a.ts, src/features/example-group/save/model/a.tspass fsd/repetitive-naming rejected #1 src/features/example-group/item-drop/model/a.ts, src/features/example-group/item-load/model/a.ts, src/features/example-group/item-save/model/a.tspass fsd/excessive-slicing accepted #1 src/widgets/example-group/footer/ui/a.ts, src/widgets/example-group/header/ui/a.tspass fsd/excessive-slicing rejected #1 src/widgets/example-group/w01/ui/a.ts, src/widgets/example-group/w02/ui/a.ts, src/widgets/example-group/w03/ui/a.ts, src/widgets/example-group/w04/ui/a.ts, src/widgets/example-group/w05/ui/a.ts, src/widgets/example-group/w06/ui/a.ts, src/widgets/example-group/w07/ui/a.ts, src/widgets/example-group/w08/ui/a.ts, src/widgets/example-group/w09/ui/a.ts, src/widgets/example-group/w10/ui/a.ts, src/widgets/example-group/w11/ui/a.ts, src/widgets/example-group/w12/ui/a.ts, src/widgets/example-group/w13/ui/a.ts, src/widgets/example-group/w14/ui/a.ts, src/widgets/example-group/w15/ui/a.ts, src/widgets/example-group/w16/ui/a.ts, src/widgets/example-group/w17/ui/a.ts, src/widgets/example-group/w18/ui/a.ts, src/widgets/example-group/w19/ui/a.ts, src/widgets/example-group/w20/ui/a.ts, src/widgets/example-group/w21/ui/a.tspass fsd/shared-lib-grouping accepted #1 src/shared/lib/example-dates/index.tspass fsd/shared-lib-grouping rejected #1 src/shared/lib/example-01.ts, src/shared/lib/example-02.ts, src/shared/lib/example-03.ts, src/shared/lib/example-04.ts, src/shared/lib/example-05.ts, src/shared/lib/example-06.ts, src/shared/lib/example-07.ts, src/shared/lib/example-08.ts, src/shared/lib/example-09.ts, src/shared/lib/example-10.ts, src/shared/lib/example-11.ts, src/shared/lib/example-12.ts, src/shared/lib/example-13.ts, src/shared/lib/example-14.ts, src/shared/lib/example-15.ts, src/shared/lib/example-16.ts33 examples, none failed; 0 rules without a rejected exampleEach rule of the preset has examples. Archangel checks them in your repository, under
root, with yourtsconfig.json. So a passing test also tells you thatrootis correct.
Fix the problems
Section titled “Fix the problems”-
Remove the import of the feature from the entity. An entity shows a product. It does not add it to a cart:
src/entities/product/ui/product-card.ts import { button } from '@/shared/ui';export function productCard(name: string) {return button(name, () => {});} -
Check again:
Terminal $ npx archangel checkerror[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.--> src/entities/product= note: rule: fsd/insignificant-slice= note: reason: A slice that at most one other slice uses belongs in that slice.= note: see: https://feature-sliced.github.io/steiger/rules/insignificant-sliceerror[plugin]: This slice has only one reference in slice "pages/home". Consider merging them.--> src/features/add-to-cart= note: rule: fsd/insignificant-slice= note: reason: A slice that at most one other slice uses belongs in that slice.= note: see: https://feature-sliced.github.io/steiger/rules/insignificant-slicechecked 12 files and 12 imports: 2 problemsfsd/widgets matches nothing in this repositoryfsd/processes matches nothing in this repositoryFor what a code means, run `archangel docs plugin`.Now no entity imports
add-to-cart, so only the page uses each slice.fsd/insignificant-slicefinds both slices.
Change the preset
Section titled “Change the preset”A store front this small has slices that only one page uses. The team decides that this is correct. It also wants the four rules of Steiger that are off by default.
-
Add
strict: trueand put the rule name inoff:archangel.config.ts import { defineConfig, fsd } from '@enhance-eng/archangel';export default defineConfig({extends: [fsd({ root: 'src', strict: true })],off: ['fsd/insignificant-slice'],});strict: trueadds the rulesfsd/no-cross-imports,fsd/no-higher-level-imports,fsd/import-localityandfsd/no-wildcard-exports. -
Check again:
Terminal $ npx archangel checkchecked 12 files and 12 imports: no problemsfsd/widgets matches nothing in this repositoryfsd/processes matches nothing in this repository
You can also change one rule of the preset:
- Remove a rule. Put its name in
off, as above.archangel ruleslists the rules thatoffremoves. - Replace a rule. Write a rule with the same name, for example
fsd/public-api. Your rule replaces the rule of the preset. - A typing error stops the check. A name in
off, and the name of a replacement rule, must match a rule of the preset. If it does not, archangel stops and shows the nearest correct name.
More than one front end
Section titled “More than one front end”To use the preset two times, give the second use its own name, for example
fsd({ root: 'apps/admin/src', name: 'fsd-admin' }). Each name in a preset starts with the
preset’s name, so the rules of the second use are fsd-admin/forbidden-imports and so on.
What changes from Steiger
Section titled “What changes from Steiger”- Archangel finds a slice by its folder. Each folder in a sliced layer is a slice. Steiger finds slices by the segments in them.
- Each rule has examples, and
archangel testruns them. - If all parts of the preset find no file, the
rootis probably wrong. Archangel showspreset-empty.
Read Migrate from Steiger to move an existing Steiger setup.
Next steps
Section titled “Next steps”- Presets lists the options of
fsdandnextjs. - Exceptions and baselines, to start on code that already breaks the rules.