@nakedev/nextjs-fsd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/bin/nextjs-fsd.js +2 -0
- package/dist/commands/add.js +182 -0
- package/dist/commands/config.js +53 -0
- package/dist/commands/generate.js +403 -0
- package/dist/commands/init.js +229 -0
- package/dist/index.js +291 -0
- package/dist/prompts.js +38 -0
- package/dist/types.js +5 -0
- package/dist/utils/config.js +86 -0
- package/dist/utils/copy.js +87 -0
- package/dist/utils/naming.js +76 -0
- package/dist/utils/project.js +288 -0
- package/dist/utils/render.js +66 -0
- package/dist/utils/version.js +23 -0
- package/package.json +66 -0
- package/templates/add/auth/auth-errors.ts.hbs +22 -0
- package/templates/add/auth/index.ts.hbs +4 -0
- package/templates/add/auth/login-form.tsx.hbs +48 -0
- package/templates/add/auth/login-index.ts.hbs +1 -0
- package/templates/add/auth/login-page.tsx.hbs +18 -0
- package/templates/add/auth/require-session.ts.hbs +29 -0
- package/templates/add/auth/session.ts.hbs +75 -0
- package/templates/add/errors/access-token.ts.hbs +19 -0
- package/templates/add/errors/api-error.ts.hbs +69 -0
- package/templates/add/errors/client.test.ts.hbs +61 -0
- package/templates/add/errors/client.ts.hbs +91 -0
- package/templates/add/errors/config-index.ts.hbs +1 -0
- package/templates/add/errors/env.ts.hbs +3 -0
- package/templates/add/errors/error-catalog.ts.hbs +19 -0
- package/templates/add/errors/error-resolver.ts.hbs +30 -0
- package/templates/add/errors/form-error.tsx.hbs +50 -0
- package/templates/add/errors/index.ts.hbs +5 -0
- package/templates/add/errors/providers.tsx.hbs +14 -0
- package/templates/add/errors/query-client.ts.hbs +25 -0
- package/templates/generate/layout/layout.tsx.hbs +20 -0
- package/templates/generate/layout/route.tsx.hbs +1 -0
- package/templates/generate/page/content.tsx.hbs +17 -0
- package/templates/generate/page/errors.ts.hbs +22 -0
- package/templates/generate/page/index.ts.hbs +1 -0
- package/templates/generate/page/page.tsx.hbs +22 -0
- package/templates/generate/page/route.tsx.hbs +3 -0
- package/templates/generate/slice/api.ts.hbs +42 -0
- package/templates/generate/slice/errors.ts.hbs +22 -0
- package/templates/generate/slice/index.ts.hbs +15 -0
- package/templates/generate/slice/lib.ts.hbs +4 -0
- package/templates/generate/slice/model.ts.hbs +10 -0
- package/templates/generate/slice/ui.tsx.hbs +17 -0
- package/templates/init/agents-section.md.hbs +25 -0
- package/templates/init/claude.md.hbs +1 -0
- package/templates/init/components.json.hbs +21 -0
- package/templates/init/eslint.fsd.mjs.hbs +91 -0
- package/templates/init/fsd.md.hbs +133 -0
- package/templates/init/globals.css.hbs +31 -0
- package/templates/init/skill.md.hbs +167 -0
- package/templates/init/steiger.config.ts.hbs +28 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// The FSD import boundary, as ESLint sees it: one file at a time, in your
|
|
2
|
+
// editor, on every save.
|
|
3
|
+
//
|
|
4
|
+
// Not a replacement for `steiger ./{{srcDir}}` and not redundant with it — the
|
|
5
|
+
// two answer different questions at different moments. A single file can show
|
|
6
|
+
// that this import points the wrong way; only a whole-tree pass can show that
|
|
7
|
+
// a slice has no references at all, that a layer is sliced too finely, or that
|
|
8
|
+
// a segment is named after its type instead of its purpose. ESLint catches the
|
|
9
|
+
// mistake as you type it; steiger catches the shape the codebase drifted into.
|
|
10
|
+
//
|
|
11
|
+
// Zero new dependencies: `no-restricted-imports` is a core rule, and the layer
|
|
12
|
+
// order is the whole of the boundary. Imports point downwards only —
|
|
13
|
+
// _app -> _pages -> widgets -> features -> entities -> shared — and a slice is
|
|
14
|
+
// always entered through its index.ts.
|
|
15
|
+
|
|
16
|
+
const LAYERS = ["_app", "_pages", "widgets", "features", "entities", "shared"];
|
|
17
|
+
|
|
18
|
+
// Layers divided into slices. `_app` and `shared` hold segments instead, so
|
|
19
|
+
// they have no sibling slices to keep apart and no index.ts to enter through.
|
|
20
|
+
const SLICED = ["_pages", "widgets", "features", "entities"];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Anything below a slice's own directory, from anywhere.
|
|
24
|
+
*
|
|
25
|
+
* One segment past the layer is the slice itself, which is exactly what
|
|
26
|
+
* another layer should import; two or more is its insides, which nothing
|
|
27
|
+
* should. That includes a file reaching into its OWN slice through the alias —
|
|
28
|
+
* inside a slice the import is relative, which is what keeps a slice movable.
|
|
29
|
+
*/
|
|
30
|
+
const insideSlice = {
|
|
31
|
+
group: SLICED.map((layer) => `{{alias}}/${layer}/*/**`),
|
|
32
|
+
message: 'reaches inside a slice — import it through its index.ts, e.g. "{{alias}}/features/<slice>".',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* One `no-restricted-imports` entry per group of files, never two.
|
|
37
|
+
*
|
|
38
|
+
* Flat config REPLACES a rule's options when a later block matches the same
|
|
39
|
+
* file; it does not merge them. So every pattern that applies to a layer has
|
|
40
|
+
* to live in that layer's single block — splitting them across blocks silently
|
|
41
|
+
* disables all but the last one to match, and the config still looks right.
|
|
42
|
+
*/
|
|
43
|
+
function boundaryFor(files, patterns) {
|
|
44
|
+
return {
|
|
45
|
+
files,
|
|
46
|
+
rules: { "no-restricted-imports": ["error", { patterns: [...patterns, insideSlice] }] },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const layerRules = LAYERS.map((layer) => {
|
|
51
|
+
const patterns = [];
|
|
52
|
+
|
|
53
|
+
const higher = LAYERS.slice(0, LAYERS.indexOf(layer));
|
|
54
|
+
if (higher.length > 0) {
|
|
55
|
+
patterns.push({
|
|
56
|
+
group: higher.map((name) => `{{alias}}/${name}/**`),
|
|
57
|
+
message:
|
|
58
|
+
`imports point downwards only, and this is a higher layer than ${layer}. ` +
|
|
59
|
+
"Move the shared code down a layer instead of reaching up.",
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (SLICED.includes(layer)) {
|
|
64
|
+
patterns.push({
|
|
65
|
+
group: [`{{alias}}/${layer}/*`],
|
|
66
|
+
message:
|
|
67
|
+
"two slices on the same layer never import each other. Move the shared code down a layer, " +
|
|
68
|
+
"or use a relative path if it really belongs to this slice.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return boundaryFor([`{{srcDir}}/${layer}/**/*.{ts,tsx}`], patterns);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const fsdBoundary = [
|
|
76
|
+
...layerRules,
|
|
77
|
+
// {{appDir}}/ is routing. A route file re-exports a page and its metadata;
|
|
78
|
+
// composing features straight into a route is how the routing layer starts
|
|
79
|
+
// growing logic that belongs in a slice.
|
|
80
|
+
boundaryFor(
|
|
81
|
+
["{{appDir}}/**/*.{ts,tsx}"],
|
|
82
|
+
[
|
|
83
|
+
{
|
|
84
|
+
group: ["{{alias}}/widgets/**", "{{alias}}/features/**", "{{alias}}/entities/**"],
|
|
85
|
+
message: "{{appDir}}/ composes pages, not features — put it in a _pages slice and re-export that.",
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
export default fsdBoundary;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Feature-Sliced Design in this project
|
|
2
|
+
|
|
3
|
+
> Generated by `nextjs-fsd init`. Edit freely — this file is the convention,
|
|
4
|
+
> not a lock file. Keep it in step with the code; a doc describing the
|
|
5
|
+
> previous shape is worse than no doc.
|
|
6
|
+
|
|
7
|
+
## Layout
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
{{appDir}}/ # Next.js App Router — routing only
|
|
11
|
+
├── layout.tsx # composition root: fonts, <Providers>, global CSS
|
|
12
|
+
└── <route>/page.tsx # re-exports an FSD page + its metadata, nothing else
|
|
13
|
+
{{srcDir}}/
|
|
14
|
+
├── _app/ # FSD app layer — providers/, styles/
|
|
15
|
+
├── _pages/<page>/ # one slice per route: ui/, index.ts (public API)
|
|
16
|
+
├── features/<slice>/ # a whole user action, once two pages need it
|
|
17
|
+
├── entities/<slice>/ # a business object, once two features need it
|
|
18
|
+
└── shared/ # infrastructure only — api/, auth/, config/, lib/, ui/
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
The FSD `app` and `pages` layers are named `_app` and `_pages` because
|
|
22
|
+
Next.js owns those names at the root.
|
|
23
|
+
|
|
24
|
+
**Add layers when a second consumer actually appears, not up front.**
|
|
25
|
+
Duplicating a component across two pages is cheaper to undo than an
|
|
26
|
+
`entities/` layer built for a reuse that never happens. FSD v2.1 discourages
|
|
27
|
+
`widgets/` — UI blocks carry user-flow logic, which makes the
|
|
28
|
+
widget/feature boundary arbitrary — so reach for `features/` first.
|
|
29
|
+
|
|
30
|
+
## Import boundary
|
|
31
|
+
|
|
32
|
+
Imports point downwards only: `_app → _pages → widgets → features → entities
|
|
33
|
+
→ shared`. Two slices on the same layer never import each other, and a slice
|
|
34
|
+
is always imported through its `index.ts`:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { LoginForm } from "{{alias}}/_pages/login"; // ✅ public API
|
|
38
|
+
import { LoginForm } from "{{alias}}/_pages/login/ui/login-form"; // ❌ reaches inside
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
If two pages need the same thing, that is the signal it was never
|
|
42
|
+
page-scoped: move it down a layer instead of importing sideways. Two copies
|
|
43
|
+
in two pages while you are still unsure is the cheaper mistake.
|
|
44
|
+
|
|
45
|
+
## Two linters, on purpose
|
|
46
|
+
|
|
47
|
+
`{{lintCommand}}` runs both, and they are not redundant:
|
|
48
|
+
|
|
49
|
+
| | catches | when |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| **ESLint** (`eslint.fsd.mjs`) | this import points the wrong way, or reaches past a slice's `index.ts` | as you type, per file, in the editor |
|
|
52
|
+
| **steiger** (`steiger.config.ts`) | a slice with no references, a layer sliced too finely, a segment named after its type, inconsistent naming | on demand, whole tree |
|
|
53
|
+
|
|
54
|
+
A single file is enough to see a bad import, so that check belongs where you
|
|
55
|
+
get it instantly. Nothing in one file can show that a slice has no consumers
|
|
56
|
+
or that a layer has thirty slices — that needs the whole tree, which is what
|
|
57
|
+
steiger walks.
|
|
58
|
+
|
|
59
|
+
`eslint.fsd.mjs` has no dependencies of its own: the boundary is expressed
|
|
60
|
+
with the core `no-restricted-imports` rule, and the layer order above is the
|
|
61
|
+
whole of it. One gotcha if you edit it — flat config **replaces** a rule's
|
|
62
|
+
options when a later block matches the same file instead of merging them, so
|
|
63
|
+
every pattern for a layer has to stay in that layer's single block. Split them
|
|
64
|
+
across two blocks and the earlier one silently stops applying.
|
|
65
|
+
|
|
66
|
+
When steiger says *"this slice has only one reference — consider merging
|
|
67
|
+
them"*, that is the "add layers when a second consumer appears" rule talking,
|
|
68
|
+
not a false positive. It is configured as a **warning** rather than an error,
|
|
69
|
+
because every slice has one consumer on the day it is created and failing the
|
|
70
|
+
build for that teaches people to delete the rule instead of the slice. Read it
|
|
71
|
+
anyway: a slice that stays at one consumer for good probably belongs inside
|
|
72
|
+
that consumer.
|
|
73
|
+
|
|
74
|
+
## `{{appDir}}/` stays thin
|
|
75
|
+
|
|
76
|
+
A route file re-exports an FSD page and nothing else:
|
|
77
|
+
|
|
78
|
+
```tsx
|
|
79
|
+
// {{appDir}}/login/page.tsx
|
|
80
|
+
export { LoginPage as default, metadata } from "{{alias}}/_pages/login";
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Export `metadata` alongside the component — a route file that only
|
|
84
|
+
re-exports `default` silently drops the page's title.
|
|
85
|
+
|
|
86
|
+
## Segments
|
|
87
|
+
|
|
88
|
+
A slice gets only the segments it has code for. `ui/` alone is the common
|
|
89
|
+
case; add `model/` (state, hooks), `api/` (requests) or `lib/` (helpers) when
|
|
90
|
+
that slice actually has them. An empty segment folder is noise.
|
|
91
|
+
|
|
92
|
+
Adding one later is the normal path, not a rewrite — re-run the same command
|
|
93
|
+
with the extra segment and it writes only what is missing, appending the new
|
|
94
|
+
exports to the slice's `index.ts` and leaving every existing file untouched:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
nextjs-fsd generate slice features checkout --segments ui # ui/ only
|
|
98
|
+
nextjs-fsd generate slice features checkout --segments ui,model # adds model/
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
{{srcDir}}/_pages/<page>/
|
|
103
|
+
├── ui/<page>-page.tsx # server component + `metadata`
|
|
104
|
+
├── ui/<thing>.tsx # "use client" only on the leaves that need it
|
|
105
|
+
└── index.ts # public API — the only thing {{appDir}}/ imports
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Server vs. client components
|
|
109
|
+
|
|
110
|
+
Default to a Server Component. `"use client"` goes on the leaf that needs
|
|
111
|
+
`useState`/`useEffect`/events — a page component stays a server component
|
|
112
|
+
while its form does not.
|
|
113
|
+
|
|
114
|
+
## Generating
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
nextjs-fsd generate page <name> # slice + {{appDir}}/<name>/page.tsx
|
|
118
|
+
nextjs-fsd generate slice features <name> # a feature slice
|
|
119
|
+
nextjs-fsd generate layout admin # _app/layouts + {{appDir}}/(admin)/layout.tsx
|
|
120
|
+
nextjs-fsd add error-handling # shared/api: ApiError, catalogs, client
|
|
121
|
+
nextjs-fsd add auth # shared/auth: session, guard, login page
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Re-running a generate command extends what is there instead of refusing, so
|
|
125
|
+
`--errors` or `--client` on a page built bare earlier just adds the file. It
|
|
126
|
+
never rewrites one you already have — and it will not add a second route file
|
|
127
|
+
for a page that is already routed from somewhere else, because two `page.tsx`
|
|
128
|
+
resolving to the same URL is a Next.js build error.
|
|
129
|
+
|
|
130
|
+
Layouts live in `_app/layouts`, not `_pages`: a layout is not one route's
|
|
131
|
+
content, it is what several routes have in common, and `{{appDir}}/(group)/layout.tsx`
|
|
132
|
+
re-exports it the same way a route file re-exports a page. `shadcn add <name>`
|
|
133
|
+
writes into `{{srcDir}}/shared/ui` — `components.json` is already aimed there.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/* This file lives in the FSD app layer, not next to the routes, so name the
|
|
4
|
+
trees Tailwind has to scan for class names explicitly instead of relying on
|
|
5
|
+
where auto-detection decides the project root is. */
|
|
6
|
+
@source "{{cssSourceApp}}";
|
|
7
|
+
@source "{{cssSourceSrc}}";
|
|
8
|
+
|
|
9
|
+
@custom-variant dark (&:is(.dark *));
|
|
10
|
+
|
|
11
|
+
@theme inline {
|
|
12
|
+
--color-background: var(--background);
|
|
13
|
+
--color-foreground: var(--foreground);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
:root {
|
|
17
|
+
--background: oklch(1 0 0);
|
|
18
|
+
--foreground: oklch(0.145 0 0);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.dark {
|
|
22
|
+
--background: oklch(0.145 0 0);
|
|
23
|
+
--foreground: oklch(0.985 0 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@layer base {
|
|
27
|
+
body {
|
|
28
|
+
background-color: var(--color-background);
|
|
29
|
+
color: var(--color-foreground);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: nextjs-fsd
|
|
3
|
+
description: >
|
|
4
|
+
Use whenever the user asks to add, create, scaffold, or wire up a new page,
|
|
5
|
+
screen, route, feature, entity, layout, or shared route shell in THIS
|
|
6
|
+
project — including indirect requests such as "add a settings screen", "let
|
|
7
|
+
staff approve loans", "put the dashboard behind login", or "show a proper
|
|
8
|
+
message when the API fails". This project is organized with Feature-Sliced
|
|
9
|
+
Design and has a nextjs-fsd.config.json at its root; check for that file
|
|
10
|
+
before assuming this applies. Also use when adding API error handling or
|
|
11
|
+
authentication for the first time. Do NOT use for business logic or markup
|
|
12
|
+
inside a file the CLI already generated, for styling, for a bug fix or
|
|
13
|
+
refactor, or for a project without nextjs-fsd.config.json.
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
# nextjs-fsd
|
|
17
|
+
|
|
18
|
+
Use this skill for the FSD surface only: which directory a new thing goes in,
|
|
19
|
+
and how it is imported. Once a slice exists, its content — markup, business
|
|
20
|
+
rules, request shapes — is the engineer's work.
|
|
21
|
+
|
|
22
|
+
Create slices with the CLI rather than by hand, so the route file, the
|
|
23
|
+
`index.ts` public API, the `"use client"` placement, and the segment layout
|
|
24
|
+
stay consistent. Hand-written slices are exactly what the two linters then
|
|
25
|
+
report.
|
|
26
|
+
|
|
27
|
+
This file is emitted at `.claude/skills/nextjs-fsd/SKILL.md`. Agents that do
|
|
28
|
+
not load `.claude/skills` should follow the root `AGENTS.md`, which points at
|
|
29
|
+
[docs/fsd.md](../../../docs/fsd.md) — the fuller convention, and the source of
|
|
30
|
+
truth if this file and it ever disagree.
|
|
31
|
+
|
|
32
|
+
**FSD methodology questions are not this skill's job.** Whether a thing should
|
|
33
|
+
be an entity at all, when to extract from a page, cross-import and `@x`
|
|
34
|
+
patterns, how strict to be — that is the `feature-sliced-design` skill
|
|
35
|
+
(fsd.how). This one only covers driving the generator in this repo.
|
|
36
|
+
|
|
37
|
+
## When to use this skill
|
|
38
|
+
|
|
39
|
+
- A new page, screen or route is requested, even without the word "page".
|
|
40
|
+
- A new reusable feature or business object is requested, even without the
|
|
41
|
+
words "feature" or "entity".
|
|
42
|
+
- Several routes need shared chrome (a sidebar, an auth card) → a layout.
|
|
43
|
+
- The project needs API error handling or auth for the first time.
|
|
44
|
+
- An existing slice needs a segment it does not have yet (state, requests,
|
|
45
|
+
helpers, an error catalog).
|
|
46
|
+
|
|
47
|
+
Examples that all use this skill:
|
|
48
|
+
|
|
49
|
+
- "add a settings screen" → `generate page settings`
|
|
50
|
+
- "the dashboard should require login" → `generate page dashboard --auth`
|
|
51
|
+
- "admin pages need a shared sidebar" → `generate layout admin`
|
|
52
|
+
- "loans need to be reusable across screens" → `generate slice entities loan`
|
|
53
|
+
- "show a real message when saving fails" → `add error-handling`, then
|
|
54
|
+
`--errors` on the slice that owns the failing call
|
|
55
|
+
|
|
56
|
+
## When not to use it
|
|
57
|
+
|
|
58
|
+
- Writing the actual markup, state, or request body inside a generated file
|
|
59
|
+
- Styling, Tailwind theme edits, or shadcn component customisation
|
|
60
|
+
- Bug fixes, refactors, or dependency work
|
|
61
|
+
- Deciding whether a layer *should* exist — that is the FSD skill
|
|
62
|
+
- Any repository without nextjs-fsd.config.json at its root
|
|
63
|
+
|
|
64
|
+
## Before running the CLI
|
|
65
|
+
|
|
66
|
+
1. Read `nextjs-fsd.config.json`. `features.errorHandling` and `features.auth`
|
|
67
|
+
decide which flags are even legal, `appDir` says whether routes live in
|
|
68
|
+
`app/` or `src/app/`, and `locale` says which language generated copy is
|
|
69
|
+
written in.
|
|
70
|
+
2. Look for the slice first. `{{srcDir}}/_pages/`, `{{srcDir}}/features/`,
|
|
71
|
+
`{{srcDir}}/entities/` — a name that already exists means you are extending,
|
|
72
|
+
not creating, and the flags differ.
|
|
73
|
+
3. Check `{{appDir}}/` for an existing route before choosing `--route`. A page
|
|
74
|
+
routed from a group is not where the default would look.
|
|
75
|
+
4. Confirm the work is on a branch you may commit to (`git status -sb`) and
|
|
76
|
+
preserve unrelated changes.
|
|
77
|
+
|
|
78
|
+
## Commands
|
|
79
|
+
|
|
80
|
+
Run from the project root. **Pass every value as a flag** — an omitted value
|
|
81
|
+
opens a prompt, and in a non-interactive shell that exits 1 without writing
|
|
82
|
+
anything.
|
|
83
|
+
|
|
84
|
+
~~~bash
|
|
85
|
+
nextjs-fsd generate page <name> [--route <path>] [--no-route] [--client] [--auth] [--errors] [--defaults]
|
|
86
|
+
nextjs-fsd generate slice <features|entities|widgets> <name> [--segments ui,model,api,lib] [--errors] [--defaults]
|
|
87
|
+
nextjs-fsd generate layout <name> [--route <path>] [--no-route] [--defaults]
|
|
88
|
+
nextjs-fsd add error-handling [-y] [--no-install]
|
|
89
|
+
nextjs-fsd add auth [-y] [--no-install]
|
|
90
|
+
nextjs-fsd config show
|
|
91
|
+
~~~
|
|
92
|
+
|
|
93
|
+
`--defaults` answers every remaining question, so scripted and agent use needs
|
|
94
|
+
`--defaults` (or the full set of flags) on every generate command.
|
|
95
|
+
|
|
96
|
+
Feature gates, which fail loudly rather than generating something broken:
|
|
97
|
+
|
|
98
|
+
- `--auth` requires `features.auth` → run `add auth` first
|
|
99
|
+
- `--errors` and the `api` segment require `features.errorHandling` →
|
|
100
|
+
run `add error-handling` first
|
|
101
|
+
- `add auth` installs error handling itself if it is missing; do not run both
|
|
102
|
+
|
|
103
|
+
## Extending, not rewriting
|
|
104
|
+
|
|
105
|
+
**Re-run the same command to add to something that exists.** Do not hand-write
|
|
106
|
+
the file, and do not delete the slice to regenerate it.
|
|
107
|
+
|
|
108
|
+
~~~bash
|
|
109
|
+
nextjs-fsd generate slice features checkout --segments ui # ui/ only
|
|
110
|
+
nextjs-fsd generate slice features checkout --segments ui,model # adds model/
|
|
111
|
+
nextjs-fsd generate page dashboard --errors --defaults # adds the catalog
|
|
112
|
+
~~~
|
|
113
|
+
|
|
114
|
+
Only the missing files are written; new exports are appended to the slice's
|
|
115
|
+
`index.ts`; every existing file is left exactly as it is. If there is nothing
|
|
116
|
+
to add, the command says so and writes nothing — that is the expected answer,
|
|
117
|
+
not a failure to work around.
|
|
118
|
+
|
|
119
|
+
Two consequences worth knowing:
|
|
120
|
+
|
|
121
|
+
- The page component is an existing file, so a leaf added later is not rendered
|
|
122
|
+
by anything yet. The command prints the one import line to add; add it.
|
|
123
|
+
- A page already routed from elsewhere does not get a second route file. Two
|
|
124
|
+
`page.tsx` resolving to one URL is a Next.js build error.
|
|
125
|
+
|
|
126
|
+
## Contracts the generated code relies on
|
|
127
|
+
|
|
128
|
+
Breaking one of these is silent — the build passes and the app is wrong.
|
|
129
|
+
|
|
130
|
+
- **A route file re-exports the page *and* its `metadata`.** `export { X as
|
|
131
|
+
default } from "..."` alone drops the page title with no error.
|
|
132
|
+
- **`"use client"` goes on the leaf, not the page.** A page component stays a
|
|
133
|
+
server component; state, effects and events live in a `ui/<thing>.tsx`.
|
|
134
|
+
- **Imports point downwards, and a slice is entered through its `index.ts`.**
|
|
135
|
+
`{{alias}}/features/x`, never `{{alias}}/features/x/ui/x`. Inside a slice the
|
|
136
|
+
import is relative.
|
|
137
|
+
- **Never render `error.message` as UI copy.** Add the code to the domain's own
|
|
138
|
+
catalog (`model/<name>-errors.ts`) and let the fallback name the action that
|
|
139
|
+
failed. A backend message is written for a log.
|
|
140
|
+
- **One catalog per domain, never one global map.** `generate ... --errors`
|
|
141
|
+
creates it in the right place.
|
|
142
|
+
- **The access token stays in the module variable** in
|
|
143
|
+
`{{srcDir}}/shared/auth/access-token.ts`. Never move it to `localStorage`, and
|
|
144
|
+
never remove the single-flight guard in `shared/api/client.ts` — a backend
|
|
145
|
+
that rotates refresh tokens logs the user out mid-session without it.
|
|
146
|
+
- **`{{appDir}}/` composes pages, not features.** ESLint enforces this one.
|
|
147
|
+
|
|
148
|
+
## Verification
|
|
149
|
+
|
|
150
|
+
After generating and filling in a slice:
|
|
151
|
+
|
|
152
|
+
~~~bash
|
|
153
|
+
{{lintCommand}} # eslint (per-file boundary) + steiger (whole-tree)
|
|
154
|
+
{{packageManager}} run build
|
|
155
|
+
~~~
|
|
156
|
+
|
|
157
|
+
ESLint reports a wrong-way or slice-internal import at the file. steiger
|
|
158
|
+
reports what one file cannot show. `fsd/insignificant-slice` on a brand-new
|
|
159
|
+
slice means nothing imports it yet — either wire it up or inline it back into
|
|
160
|
+
its single consumer; it is the linter working, not a false positive.
|
|
161
|
+
|
|
162
|
+
Where the CLI wrote a `client.test.ts`, run the project's test command too. Do
|
|
163
|
+
not delete that test to make a change pass: the single-flight 401 refresh and
|
|
164
|
+
the no-refresh-on-`/auth/*` rule are the two things in this codebase that fail
|
|
165
|
+
silently in a browser.
|
|
166
|
+
|
|
167
|
+
Report the exact output, and say plainly which generated TODOs are still TODOs.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import fsd from "@feature-sliced/steiger-plugin";
|
|
2
|
+
import { defineConfig } from "steiger";
|
|
3
|
+
|
|
4
|
+
export default defineConfig([
|
|
5
|
+
...fsd.configs.recommended,
|
|
6
|
+
{
|
|
7
|
+
// `app/` and `pages/` are Next.js' own directory names, so the FSD layers
|
|
8
|
+
// take an `_` prefix. Scoped off, not global — a typo in a future layer
|
|
9
|
+
// name still gets flagged.
|
|
10
|
+
files: ["./{{srcDir}}/_app/**", "./{{srcDir}}/_pages/**"],
|
|
11
|
+
rules: { "fsd/typo-in-layer-name": "off" },
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
// The Next.js FSD convention names this segment `providers`.
|
|
15
|
+
files: ["./{{srcDir}}/_app/providers/**"],
|
|
16
|
+
rules: { "fsd/segments-by-purpose": "off" },
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
// A warning, not an error, because at default severity this fails `lint`
|
|
20
|
+
// on the structure FSD's own guidance calls the normal starting point: a
|
|
21
|
+
// slice extracted for its first consumer, before the second one exists.
|
|
22
|
+
// The message is still worth reading — a slice that stays at one consumer
|
|
23
|
+
// for good probably belongs inside it — but a fresh slice failing CI
|
|
24
|
+
// teaches people to delete the linter rather than the slice.
|
|
25
|
+
files: ["./{{srcDir}}/**"],
|
|
26
|
+
rules: { "fsd/insignificant-slice": "warn" },
|
|
27
|
+
},
|
|
28
|
+
]);
|