@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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +165 -0
  3. package/bin/nextjs-fsd.js +2 -0
  4. package/dist/commands/add.js +182 -0
  5. package/dist/commands/config.js +53 -0
  6. package/dist/commands/generate.js +403 -0
  7. package/dist/commands/init.js +229 -0
  8. package/dist/index.js +291 -0
  9. package/dist/prompts.js +38 -0
  10. package/dist/types.js +5 -0
  11. package/dist/utils/config.js +86 -0
  12. package/dist/utils/copy.js +87 -0
  13. package/dist/utils/naming.js +76 -0
  14. package/dist/utils/project.js +288 -0
  15. package/dist/utils/render.js +66 -0
  16. package/dist/utils/version.js +23 -0
  17. package/package.json +66 -0
  18. package/templates/add/auth/auth-errors.ts.hbs +22 -0
  19. package/templates/add/auth/index.ts.hbs +4 -0
  20. package/templates/add/auth/login-form.tsx.hbs +48 -0
  21. package/templates/add/auth/login-index.ts.hbs +1 -0
  22. package/templates/add/auth/login-page.tsx.hbs +18 -0
  23. package/templates/add/auth/require-session.ts.hbs +29 -0
  24. package/templates/add/auth/session.ts.hbs +75 -0
  25. package/templates/add/errors/access-token.ts.hbs +19 -0
  26. package/templates/add/errors/api-error.ts.hbs +69 -0
  27. package/templates/add/errors/client.test.ts.hbs +61 -0
  28. package/templates/add/errors/client.ts.hbs +91 -0
  29. package/templates/add/errors/config-index.ts.hbs +1 -0
  30. package/templates/add/errors/env.ts.hbs +3 -0
  31. package/templates/add/errors/error-catalog.ts.hbs +19 -0
  32. package/templates/add/errors/error-resolver.ts.hbs +30 -0
  33. package/templates/add/errors/form-error.tsx.hbs +50 -0
  34. package/templates/add/errors/index.ts.hbs +5 -0
  35. package/templates/add/errors/providers.tsx.hbs +14 -0
  36. package/templates/add/errors/query-client.ts.hbs +25 -0
  37. package/templates/generate/layout/layout.tsx.hbs +20 -0
  38. package/templates/generate/layout/route.tsx.hbs +1 -0
  39. package/templates/generate/page/content.tsx.hbs +17 -0
  40. package/templates/generate/page/errors.ts.hbs +22 -0
  41. package/templates/generate/page/index.ts.hbs +1 -0
  42. package/templates/generate/page/page.tsx.hbs +22 -0
  43. package/templates/generate/page/route.tsx.hbs +3 -0
  44. package/templates/generate/slice/api.ts.hbs +42 -0
  45. package/templates/generate/slice/errors.ts.hbs +22 -0
  46. package/templates/generate/slice/index.ts.hbs +15 -0
  47. package/templates/generate/slice/lib.ts.hbs +4 -0
  48. package/templates/generate/slice/model.ts.hbs +10 -0
  49. package/templates/generate/slice/ui.tsx.hbs +17 -0
  50. package/templates/init/agents-section.md.hbs +25 -0
  51. package/templates/init/claude.md.hbs +1 -0
  52. package/templates/init/components.json.hbs +21 -0
  53. package/templates/init/eslint.fsd.mjs.hbs +91 -0
  54. package/templates/init/fsd.md.hbs +133 -0
  55. package/templates/init/globals.css.hbs +31 -0
  56. package/templates/init/skill.md.hbs +167 -0
  57. package/templates/init/steiger.config.ts.hbs +28 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 nakedev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,165 @@
1
+ # @nakedev/nextjs-fsd
2
+
3
+ Keep a Next.js App Router project on [Feature-Sliced Design](https://feature-sliced.design/).
4
+
5
+ Next.js already creates the app, so this CLI does not. It shapes what is
6
+ inside one: `init` once, then `generate` for slices and `add` for the two
7
+ pieces of wiring every project rewrites by hand — API error handling and auth.
8
+
9
+ Sibling to [`go-scaffold`](https://github.com/NakePranob/go-scaffold), which
10
+ generates the Go backend these templates are written against.
11
+
12
+ ```bash
13
+ bunx create-next-app@latest my-app --ts --app --tailwind
14
+ cd my-app
15
+
16
+ bunx @nakedev/nextjs-fsd init
17
+ bunx @nakedev/nextjs-fsd add auth
18
+ bunx @nakedev/nextjs-fsd generate page dashboard --auth
19
+ ```
20
+
21
+ ## Commands
22
+
23
+ | Command | What it does |
24
+ |---|---|
25
+ | `nextjs-fsd` | menu — asks what to do, then delegates |
26
+ | `init` | FSD layers in `src/`, the `@/*` alias, steiger + its config, `docs/fsd.md` |
27
+ | `generate page [name]` | a `_pages` slice plus the thin `app/<route>/page.tsx` that re-exports it |
28
+ | `generate slice [layer] [name]` | a `features`/`entities`/`widgets` slice with only the segments it needs |
29
+ | `generate layout [name]` | a shared route shell in `_app/layouts` plus the `layout.tsx` that re-exports it |
30
+ | `add error-handling` | `shared/api` — `ApiError`, per-domain error catalogs, axios client, `QueryClient` |
31
+ | `add auth` | `shared/auth` — in-memory access token, session hooks, route guard, login page |
32
+ | `config show` | the resolved config and which features are installed |
33
+ | `config set locale <th\|en>` | the language future generated copy is written in |
34
+
35
+ Every command asks for what you leave out. `--defaults` answers every
36
+ question, for CI.
37
+
38
+ **Nothing is ever overwritten.** Re-running a generate command on something
39
+ that exists *extends* it — `--segments ui,model` on a ui-only slice writes
40
+ `model/` and appends its exports to `index.ts`, leaving every existing file
41
+ alone. If there is nothing new to add, it says so rather than writing. Adding
42
+ a segment later is the normal path, which is the whole point of "a slice gets
43
+ only the segments it has code for".
44
+
45
+ It also will not add a second route file for a page that is already routed
46
+ from somewhere else — two `page.tsx` resolving to the same URL is a Next.js
47
+ build error, and a page generated with `--route "(admin)/dashboard"` is not
48
+ where the default would look.
49
+
50
+ ## What `init` does to an existing project
51
+
52
+ - moves `app/globals.css` to `src/_app/styles/globals.css` and names the
53
+ `@source` trees Tailwind now has to scan, then repoints the import in
54
+ `layout.tsx`
55
+ - puts `./src/*` first in the tsconfig `@/*` alias, keeping whatever was there
56
+ as a fallback so existing `@/…` imports keep resolving
57
+ - writes `eslint.fsd.mjs` and spreads it into `eslint.config.mjs`, so a
58
+ wrong-way or slice-internal import is flagged per file in the editor — the
59
+ core `no-restricted-imports` rule, no new dependency
60
+ - adds `steiger` and `steiger.config.ts` (with the `_app`/`_pages` layer-name
61
+ rule turned off) and chains both into the `lint` script
62
+ - writes `components.json` so `shadcn add <name>` lands in `src/shared/ui`
63
+ and its `utils.ts` in `src/shared/lib` — `shadcn init`'s own defaults put
64
+ them in `./components/ui` and the project root, outside the layers entirely
65
+ - writes `docs/fsd.md`, a `.claude/skills/nextjs-fsd/SKILL.md` skill, and
66
+ appends a section to `AGENTS.md` pointing at both — so an agent asked to "add
67
+ a settings screen" reaches for the generator instead of hand-writing the
68
+ files the linters then report
69
+
70
+ It creates no empty layer directories — `features/` and `entities/` appear
71
+ when a slice actually needs them, which is the FSD advice, not a shortcut.
72
+
73
+ ## Two linters, on purpose
74
+
75
+ ESLint and steiger overlap on paper and not in practice:
76
+
77
+ | | catches | when |
78
+ |---|---|---|
79
+ | ESLint | this import points the wrong way, or reaches past a slice's `index.ts` | as you type, per file |
80
+ | steiger | a slice with no references, a layer sliced too finely, a segment named after its type | on demand, whole tree |
81
+
82
+ A bad import is visible in one file, so that check belongs where it is
83
+ instant. Nothing in one file shows that a slice has no consumers — that needs
84
+ the whole tree. `init` sets up both and chains them into `lint`.
85
+
86
+ The generated ESLint block has one trap worth knowing if you edit it: flat
87
+ config **replaces** a rule's options when a later block matches the same file
88
+ rather than merging them, so all of a layer's `no-restricted-imports` patterns
89
+ have to stay in that layer's single block. A test pins this down, because a
90
+ split config still reads as though both halves applied.
91
+
92
+ ## The two `add` targets
93
+
94
+ `add error-handling` is the pattern rather than a library: one `ApiError` at
95
+ the boundary, an axios client whose 401 refresh is **single-flight** (a
96
+ backend that rotates refresh tokens logs the user out mid-session otherwise),
97
+ and error copy that lives in a per-domain catalog instead of one global map.
98
+ `error.message` is never rendered — a backend message is written for a log,
99
+ and an unmapped code falls through to a fallback that names the action that
100
+ failed.
101
+
102
+ Give each domain its own catalog with `--errors` on either generate command.
103
+
104
+ `add auth` needs that client, so it installs it first if it is missing. It
105
+ scaffolds password login only: access token in a module variable (never
106
+ `localStorage`), refresh token left to the API's httpOnly cookie,
107
+ `useRequireSession` as UX with the real gate still on the server. MFA, OAuth
108
+ providers and RBAC are not scaffolded.
109
+
110
+ Both assume the shape `go-scaffold` produces — `POST /auth/login`,
111
+ `POST /auth/refresh`, `GET /users/me`, and an `{ error: { code, message } }`
112
+ envelope. Adjust the paths and the `Session` type if yours differ; they are
113
+ one file each.
114
+
115
+ ## Generated copy
116
+
117
+ `init` asks whether the user-facing strings should be Thai or English
118
+ (`--locale th|en`, Thai by default) and records it. It only decides what the
119
+ first draft reads like — the catalogs are meant to be edited, and
120
+ `config set locale` changes the setting for future generation without
121
+ rewriting anything already on disk.
122
+
123
+ For a Thai project, `generate page` also asks for the page title, because the
124
+ Title Case of a kebab name is the right answer in English and the wrong one in
125
+ Thai. `--title` skips the question.
126
+
127
+ Thai copy also needs a Thai face: `init` warns that create-next-app leaves
128
+ `font-family: Arial, Helvetica, sans-serif` on `body`, which has no Thai
129
+ coverage at all, and names the `next/font` fix. It does not rewrite your font
130
+ stack — that choice is yours.
131
+
132
+ ## Requirements
133
+
134
+ Node 20.9+, a Next.js 15/16 App Router project in TypeScript. Both
135
+ create-next-app layouts work as-is (`app/` and `src/app/`) — FSD's own app
136
+ layer is `src/_app`, so it never collides with either.
137
+
138
+ The generated `client.test.ts` is written only for bun projects: `bun test`
139
+ resolves the `@/` alias with no config, while node and vitest need a runner
140
+ set up first, which is not this CLI's business.
141
+
142
+ ## Development
143
+
144
+ ```bash
145
+ pnpm install
146
+ pnpm run verify # build + unit tests + smoke test — fast, offline
147
+ pnpm run test:integration # real create-next-app + install + next build — slow, networked
148
+ ```
149
+
150
+ `scripts/smoke-test.mjs` drives the actual binary through
151
+ init → add → generate → extend against three fixtures (root `app/`, `src/app/`,
152
+ and a bun-shaped one), checks what came out, asserts no `{{…}}` template
153
+ syntax leaked, and **type-checks the generated project** by symlinking this
154
+ repo's `node_modules` into the fixture. That last step is why `next` and
155
+ `react` are devDependencies here; they are never shipped.
156
+
157
+ `scripts/integration-test.mjs` is the layer that catches what the smoke test
158
+ structurally cannot — a dependency range that does not resolve (the fixture
159
+ borrows packages it never installed) and Next.js behaviour drift. It costs
160
+ minutes and a network, so it runs on demand and in CI.
161
+
162
+ The unit tests in `tests/` cover the file patchers in isolation, where the
163
+ fiddly cases live.
164
+
165
+ MIT.
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require("../dist/index.js");
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.addErrorHandling = addErrorHandling;
7
+ exports.addAuth = addAuth;
8
+ const path_1 = __importDefault(require("path"));
9
+ const fs_extra_1 = __importDefault(require("fs-extra"));
10
+ const picocolors_1 = __importDefault(require("picocolors"));
11
+ const prompts_1 = require("../prompts");
12
+ const config_1 = require("../utils/config");
13
+ const copy_1 = require("../utils/copy");
14
+ const render_1 = require("../utils/render");
15
+ const project_1 = require("../utils/project");
16
+ const init_1 = require("./init");
17
+ const API_DEPS = {
18
+ "@tanstack/react-query": "^5.101.4",
19
+ axios: "^1.19.0",
20
+ };
21
+ // confirmAdd prints what an add is about to do and asks before it happens: it
22
+ // writes a dozen files and patches the root layout, and there is no `undo` to
23
+ // walk that back. Defaults to yes here (unlike a delete) because getting to
24
+ // this prompt already required typing the command.
25
+ async function confirmAdd(lines, opts) {
26
+ if (opts.yes)
27
+ return;
28
+ console.log(picocolors_1.default.bold("\nThis will:"));
29
+ for (const line of lines)
30
+ console.log(` ${picocolors_1.default.dim("•")} ${line}`);
31
+ console.log();
32
+ if (!(await (0, prompts_1.confirm)({ message: "Proceed?", default: true }))) {
33
+ throw new Error("cancelled — nothing was written");
34
+ }
35
+ }
36
+ /** Returns the dependencies added to package.json, so a caller that chains
37
+ * another add can install once at the end instead of twice. */
38
+ async function addErrorHandling(opts) {
39
+ const projectDir = process.cwd();
40
+ const config = (0, config_1.readConfig)(projectDir);
41
+ if (config.features.errorHandling) {
42
+ throw new Error(`error handling is already installed (${config.srcDir}/shared/api/client.ts exists) — edit the catalogs there, or delete the directory to reinstall`);
43
+ }
44
+ const providersFile = `${config.srcDir}/_app/providers/index.tsx`;
45
+ const ownsProviders = !fs_extra_1.default.existsSync(path_1.default.join(projectDir, providersFile));
46
+ await confirmAdd([
47
+ `add ${picocolors_1.default.cyan(`${config.srcDir}/shared/api/`)} — ApiError, the error catalog + resolver, an axios client with a single-flight 401 refresh, and a QueryClient`,
48
+ `add ${picocolors_1.default.cyan(`${config.srcDir}/shared/ui/form-error.tsx`)} and ${picocolors_1.default.cyan(`${config.srcDir}/shared/config/env.ts`)}`,
49
+ `add ${picocolors_1.default.cyan(`${config.srcDir}/shared/auth/access-token.ts`)} — the in-memory token the request interceptor reads (\`add auth\` fills in the rest)`,
50
+ ownsProviders
51
+ ? `add ${picocolors_1.default.cyan(providersFile)} and wrap ${config.appDir}/layout.tsx in <Providers>`
52
+ : picocolors_1.default.yellow(`leave your existing ${providersFile} alone — you add <QueryClientProvider> to it yourself`),
53
+ `add ${Object.keys(API_DEPS).join(" + ")} to package.json`,
54
+ ], opts);
55
+ // The refresh rules are the part that fails silently in a browser, so they
56
+ // get the one test — but only where it runs with no extra setup. `bun test`
57
+ // resolves the tsconfig alias on its own; node:test and vitest both need
58
+ // config this CLI has no business writing into someone's project.
59
+ const writesTest = config.packageManager === "bun";
60
+ const context = errorContext(config);
61
+ const entries = [
62
+ { template: "add/errors/api-error.ts.hbs", output: `${config.srcDir}/shared/api/api-error.ts` },
63
+ { template: "add/errors/error-catalog.ts.hbs", output: `${config.srcDir}/shared/api/error-catalog.ts` },
64
+ { template: "add/errors/error-resolver.ts.hbs", output: `${config.srcDir}/shared/api/error-resolver.ts` },
65
+ { template: "add/errors/client.ts.hbs", output: `${config.srcDir}/shared/api/client.ts` },
66
+ { template: "add/errors/query-client.ts.hbs", output: `${config.srcDir}/shared/api/query-client.ts` },
67
+ { template: "add/errors/index.ts.hbs", output: `${config.srcDir}/shared/api/index.ts` },
68
+ { template: "add/errors/access-token.ts.hbs", output: `${config.srcDir}/shared/auth/access-token.ts` },
69
+ { template: "add/errors/env.ts.hbs", output: `${config.srcDir}/shared/config/env.ts` },
70
+ { template: "add/errors/config-index.ts.hbs", output: `${config.srcDir}/shared/config/index.ts` },
71
+ { template: "add/errors/form-error.tsx.hbs", output: `${config.srcDir}/shared/ui/form-error.tsx` },
72
+ { template: "add/errors/providers.tsx.hbs", output: providersFile, when: () => ownsProviders },
73
+ {
74
+ template: "add/errors/client.test.ts.hbs",
75
+ output: `${config.srcDir}/shared/api/client.test.ts`,
76
+ when: () => writesTest,
77
+ },
78
+ ];
79
+ const written = await (0, render_1.applyTemplates)(projectDir, entries, context);
80
+ if ((0, project_1.appendExport)(projectDir, `${config.srcDir}/shared/ui/index.ts`, 'export { FormError } from "./form-error";')) {
81
+ written.push(`${config.srcDir}/shared/ui/index.ts`);
82
+ }
83
+ if ((0, project_1.appendEnvExample)(projectDir, "NEXT_PUBLIC_API_URL", "# Base URL of the API, including whatever prefix it mounts its routes under.\n" +
84
+ "# Whatever origin this app runs on must also be allowed by the API's CORS\n" +
85
+ "# config, or the browser drops the refresh cookie.\n" +
86
+ "NEXT_PUBLIC_API_URL=http://localhost:8080/api\n")) {
87
+ written.push(".env.example");
88
+ }
89
+ const layoutPatch = ownsProviders ? (0, project_1.patchLayoutProviders)(projectDir, config.appDir, config.alias) : "already";
90
+ if (layoutPatch === "patched")
91
+ written.push(`${config.appDir}/layout.tsx (<Providers>)`);
92
+ const added = (0, project_1.addDependencies)(projectDir, API_DEPS);
93
+ if (writesTest) {
94
+ // next build type-checks every file under the project, the generated test
95
+ // included — without these types `bun:test` is an unresolved module and
96
+ // the production build fails on a file that only ever runs in bun.
97
+ added.push(...(0, project_1.addDependencies)(projectDir, { "@types/bun": "^1.3.14" }, "devDependencies"));
98
+ if ((0, project_1.appendScript)(projectDir, "test", "bun test"))
99
+ written.push("package.json (test script)");
100
+ }
101
+ (0, config_1.setFeature)(projectDir, "errorHandling", true);
102
+ (0, init_1.report)(written, added);
103
+ if (!ownsProviders) {
104
+ console.log(picocolors_1.default.yellow(`\n${providersFile} already exists — wrap its children yourself:`) +
105
+ `\n import { QueryClientProvider } from "@tanstack/react-query";` +
106
+ `\n import { makeQueryClient } from "${config.alias}/shared/api";` +
107
+ `\n const [queryClient] = useState(makeQueryClient); // not a module constant` +
108
+ `\n <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>`);
109
+ }
110
+ else if (layoutPatch === "manual") {
111
+ console.log(picocolors_1.default.yellow(`\ncould not find {children} in ${config.appDir}/layout.tsx — wrap it in <Providers> by hand:`) +
112
+ `\n import { Providers } from "${config.alias}/_app/providers";`);
113
+ }
114
+ if (!writesTest) {
115
+ console.log(picocolors_1.default.dim(`\nno client.test.ts written: \`bun test\` resolves the ${config.alias}/ alias with no config, ` +
116
+ `${config.packageManager} needs a runner set up first. The single-flight refresh is the part worth pinning down when you add one.`));
117
+ }
118
+ finish(projectDir, config, added, opts);
119
+ console.log(`\n${picocolors_1.default.bold("Next:")} render a failure with ${picocolors_1.default.cyan("<FormError error={mutation.error} />")}, ` +
120
+ `and give each domain its own catalog with ${picocolors_1.default.cyan("nextjs-fsd generate page <name> --errors")}`);
121
+ return added;
122
+ }
123
+ async function addAuth(opts) {
124
+ const projectDir = process.cwd();
125
+ let config = (0, config_1.readConfig)(projectDir);
126
+ if (config.features.auth) {
127
+ throw new Error(`auth is already installed (${config.srcDir}/shared/auth/session.ts exists) — edit it there, or delete the directory to reinstall`);
128
+ }
129
+ // Not an error like `add rbac`'s "run add auth first": auth cannot work
130
+ // without the client at all — every hook below goes through it — so there is
131
+ // no choice to offer, only a step to take first.
132
+ let added = [];
133
+ if (!config.features.errorHandling) {
134
+ console.log(picocolors_1.default.dim("auth needs the API client, which `add error-handling` installs — doing that first.\n"));
135
+ // install: false so the two adds share one install run at the end.
136
+ added = await addErrorHandling({ ...opts, install: false });
137
+ config = (0, config_1.readConfig)(projectDir);
138
+ console.log();
139
+ }
140
+ await confirmAdd([
141
+ `add ${picocolors_1.default.cyan(`${config.srcDir}/shared/auth/`)} — session hooks (useSession/useLogin/useLogout), useRequireSession, and an auth error catalog`,
142
+ `add ${picocolors_1.default.cyan(`${config.srcDir}/_pages/login/`)} and ${picocolors_1.default.cyan(`${config.appDir}/login/page.tsx`)}`,
143
+ picocolors_1.default.yellow("assumes the API answers POST /auth/login with an access token, keeps the refresh token in an httpOnly cookie, and serves GET /users/me — adjust the paths and the Session type if yours differ"),
144
+ picocolors_1.default.dim("password login only: MFA, OAuth providers and RBAC are not scaffolded"),
145
+ ], opts);
146
+ const context = errorContext(config);
147
+ const auth = `${config.srcDir}/shared/auth`;
148
+ const slice = `${config.srcDir}/_pages/login`;
149
+ const written = await (0, render_1.applyTemplates)(projectDir, [
150
+ { template: "add/auth/session.ts.hbs", output: `${auth}/session.ts` },
151
+ { template: "add/auth/require-session.ts.hbs", output: `${auth}/require-session.ts` },
152
+ { template: "add/auth/auth-errors.ts.hbs", output: `${auth}/auth-errors.ts` },
153
+ { template: "add/auth/index.ts.hbs", output: `${auth}/index.ts` },
154
+ { template: "add/auth/login-index.ts.hbs", output: `${slice}/index.ts` },
155
+ { template: "add/auth/login-page.tsx.hbs", output: `${slice}/ui/login-page.tsx` },
156
+ { template: "add/auth/login-form.tsx.hbs", output: `${slice}/ui/login-form.tsx` },
157
+ { template: "generate/page/route.tsx.hbs", output: path_1.default.posix.join(config.appDir, "login", "page.tsx") },
158
+ ], { ...context, name: "login", pascal: "Login" });
159
+ (0, config_1.setFeature)(projectDir, "auth", true);
160
+ (0, init_1.report)(written);
161
+ finish(projectDir, config, added, opts);
162
+ console.log(`\n${picocolors_1.default.bold("Next:")} put a page behind the session guard with ` +
163
+ picocolors_1.default.cyan("nextjs-fsd generate page dashboard --auth"));
164
+ }
165
+ function errorContext(config) {
166
+ const copy = (0, copy_1.copyFor)(config.locale);
167
+ return {
168
+ ...config,
169
+ copy,
170
+ commonCatalogEntries: (0, copy_1.asCatalogEntries)(copy.common),
171
+ authCatalogEntries: (0, copy_1.asCatalogEntries)(copy.auth),
172
+ };
173
+ }
174
+ function finish(projectDir, config, added, opts) {
175
+ if (added.length === 0)
176
+ return;
177
+ if (opts.install === false) {
178
+ console.log(picocolors_1.default.yellow(`\nrun \`${config.packageManager} install\` to install: ${added.join(", ")}`));
179
+ return;
180
+ }
181
+ (0, project_1.installDependencies)(projectDir, config.packageManager);
182
+ }
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.showProjectConfig = showProjectConfig;
7
+ exports.setProjectLocale = setProjectLocale;
8
+ const picocolors_1 = __importDefault(require("picocolors"));
9
+ const config_1 = require("../utils/config");
10
+ const copy_1 = require("../utils/copy");
11
+ const version_1 = require("../utils/version");
12
+ function showProjectConfig() {
13
+ const config = (0, config_1.readConfig)(process.cwd());
14
+ const rows = [
15
+ ["FSD layers", `${config.srcDir}/ (_app, _pages, features, entities, shared)`],
16
+ ["App Router", `${config.appDir}/`],
17
+ ["import alias", `${config.alias}/* -> ./${config.srcDir}/*`],
18
+ ["copy language", config.locale],
19
+ ["package manager", config.packageManager],
20
+ ["error handling", config.features.errorHandling ? picocolors_1.default.green("installed") : picocolors_1.default.dim("not installed")],
21
+ ["auth", config.features.auth ? picocolors_1.default.green("installed") : picocolors_1.default.dim("not installed")],
22
+ ];
23
+ console.log();
24
+ for (const [label, value] of rows)
25
+ console.log(` ${label.padEnd(16)} ${value}`);
26
+ const stamped = config.scaffoldVersion;
27
+ const current = (0, version_1.cliVersion)();
28
+ if (stamped && stamped !== current) {
29
+ console.log(picocolors_1.default.dim(`\n scaffolded with nextjs-fsd ${stamped}, running ${current} — templates may have moved on since.`));
30
+ }
31
+ console.log();
32
+ }
33
+ /**
34
+ * Changes the language future generated copy is written in.
35
+ *
36
+ * Future only, and deliberately so: the catalogs and page titles already on
37
+ * disk are meant to be edited, and rewriting them would throw away the wording
38
+ * someone chose. The setting decides what the next draft reads like, nothing
39
+ * more.
40
+ */
41
+ function setProjectLocale(value) {
42
+ const locale = (0, copy_1.parseLocale)(value);
43
+ if (locale === undefined)
44
+ throw new Error('`config set locale` needs a value — "th" or "en"');
45
+ const config = (0, config_1.readConfig)(process.cwd());
46
+ if (config.locale === locale) {
47
+ console.log(picocolors_1.default.dim(`\nalready ${locale} — nothing to change.\n`));
48
+ return;
49
+ }
50
+ (0, config_1.writeConfig)(process.cwd(), { ...config, locale });
51
+ console.log(`\n ${picocolors_1.default.green("~")} nextjs-fsd.config.json: locale ${picocolors_1.default.dim(config.locale)} -> ${picocolors_1.default.green(locale)}\n` +
52
+ picocolors_1.default.dim(" Applies to what gets generated from now on. Catalogs and titles already written are left as they are.\n"));
53
+ }