@yousxlfs/next-arch 0.1.1 → 0.2.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 (96) hide show
  1. package/dist/index.js +581 -84
  2. package/dist/index.js.map +1 -1
  3. package/package.json +1 -1
  4. package/templates/app/package.json +25 -4
  5. package/templates/packages/better-auth/examples/src/features/_examples/with-better-auth/lib/auth-placeholder.ts +14 -0
  6. package/templates/packages/env/core/src/shared/config/env.ts +22 -0
  7. package/templates/packages/jotai/core/src/shared/providers/JotaiProvider.tsx +19 -0
  8. package/templates/packages/jotai/examples/src/features/_examples/with-jotai/README.md +3 -0
  9. package/templates/packages/jotai/examples/src/features/_examples/with-jotai/model/example.atoms.ts +19 -0
  10. package/templates/packages/motion/core/src/shared/lib/motion.ts +17 -0
  11. package/templates/packages/motion/examples/src/features/_examples/with-motion/components/ExampleMotionCard.tsx +19 -0
  12. package/templates/packages/next-intl/core/src/shared/config/i18n.ts +10 -0
  13. package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/README.md +3 -0
  14. package/templates/packages/nuqs/examples/src/features/_examples/with-nuqs/hooks/use-example-params.ts +19 -0
  15. package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/README.md +1 -0
  16. package/templates/packages/react-hook-form/examples/src/features/_examples/with-react-hook-form/components/ExampleRhfForm.tsx +40 -0
  17. package/templates/packages/redux/core/src/app/providers/redux-store.ts +15 -0
  18. package/templates/packages/redux/core/src/shared/providers/ReduxProvider.tsx +24 -0
  19. package/templates/packages/redux/examples/src/app/providers/redux-store.ts +18 -0
  20. package/templates/packages/redux/examples/src/features/_examples/with-redux/README.md +4 -0
  21. package/templates/packages/redux/examples/src/features/_examples/with-redux/model/example.slice.ts +36 -0
  22. package/templates/packages/sentry/core/src/shared/config/sentry.ts +9 -0
  23. package/templates/packages/sonner/examples/src/features/_examples/with-sonner/lib/toast.ts +7 -0
  24. package/templates/packages/sonner-provider/core/src/shared/providers/SonnerToaster.tsx +13 -0
  25. package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/README.md +3 -0
  26. package/templates/packages/tanstack-form/examples/src/features/_examples/with-tanstack-form/components/ExampleForm.tsx +60 -0
  27. package/templates/packages/tanstack-query/core/src/shared/lib/query-client.ts +24 -0
  28. package/templates/packages/tanstack-query/core/src/shared/providers/QueryProvider.tsx +30 -0
  29. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/README.md +34 -0
  30. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/actions/example.action.ts +34 -0
  31. package/templates/packages/tanstack-query/examples/src/features/_examples/with-tanstack-query/queries/use-example.query.ts +49 -0
  32. package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/README.md +3 -0
  33. package/templates/packages/tanstack-table/examples/src/features/_examples/with-tanstack-table/components/ExampleTable.tsx +66 -0
  34. package/templates/packages/trpc/core/src/app/api/trpc/router.ts +19 -0
  35. package/templates/packages/trpc/examples/src/app/providers/trpc-client.ts +13 -0
  36. package/templates/packages/uploadthing/core/src/app/api/uploadthing/route.ts +10 -0
  37. package/templates/packages/zustand/core/src/shared/lib/store.ts +13 -0
  38. package/templates/packages/zustand/examples/src/features/_examples/with-zustand/README.md +13 -0
  39. package/templates/packages/zustand/examples/src/features/_examples/with-zustand/model/example.store.ts +28 -0
  40. package/templates/pages/auth/src/app/({{name}})/layout.tsx +7 -0
  41. package/templates/pages/auth/src/app/({{name}})/login/page.tsx +5 -0
  42. package/templates/pages/auth/src/app/({{name}})/register/page.tsx +5 -0
  43. package/templates/pages/auth/src/entities/user/index.ts +2 -0
  44. package/templates/pages/auth/src/entities/user/lib/user-schema.ts +9 -0
  45. package/templates/pages/auth/src/entities/user/types/user.types.ts +5 -0
  46. package/templates/pages/auth/src/features/{{name}}/actions/login.action.ts +7 -0
  47. package/templates/pages/auth/src/features/{{name}}/actions/logout.action.ts +5 -0
  48. package/templates/pages/auth/src/features/{{name}}/actions/register.action.ts +7 -0
  49. package/templates/pages/auth/src/features/{{name}}/components/AuthGuard.tsx +14 -0
  50. package/templates/pages/auth/src/features/{{name}}/components/LoginForm.tsx +36 -0
  51. package/templates/pages/auth/src/features/{{name}}/components/RegisterForm.tsx +43 -0
  52. package/templates/pages/auth/src/features/{{name}}/hooks/use-session.ts +14 -0
  53. package/templates/pages/auth/src/features/{{name}}/index.ts +9 -0
  54. package/templates/pages/auth/src/features/{{name}}/lib/auth-helpers.ts +3 -0
  55. package/templates/pages/auth/src/features/{{name}}/queries/use-user.query.ts +16 -0
  56. package/templates/pages/auth/src/features/{{name}}/types/auth.types.ts +13 -0
  57. package/templates/pages/auth/src/views/{{name}}/LoginView.tsx +10 -0
  58. package/templates/pages/auth/src/views/{{name}}/RegisterView.tsx +10 -0
  59. package/templates/pages/auth/src/views/{{name}}/index.ts +2 -0
  60. package/templates/pages/blank/src/app/{{name}}/page.tsx +5 -0
  61. package/templates/pages/blank/src/features/{{name}}/index.ts +3 -0
  62. package/templates/pages/blank/src/views/{{name}}/index.ts +1 -0
  63. package/templates/pages/blank/src/views/{{name}}/{{Name}}View.tsx +8 -0
  64. package/templates/pages/crud/src/app/{{name}}/[id]/page.tsx +5 -0
  65. package/templates/pages/crud/src/app/{{name}}/new/page.tsx +5 -0
  66. package/templates/pages/crud/src/app/{{name}}/page.tsx +5 -0
  67. package/templates/pages/crud/src/entities/{{name}}/index.ts +2 -0
  68. package/templates/pages/crud/src/entities/{{name}}/lib/{{name}}-schema.ts +6 -0
  69. package/templates/pages/crud/src/entities/{{name}}/types/{{name}}.types.ts +4 -0
  70. package/templates/pages/crud/src/features/{{name}}/actions/create-{{name}}.action.ts +5 -0
  71. package/templates/pages/crud/src/features/{{name}}/actions/delete-{{name}}.action.ts +5 -0
  72. package/templates/pages/crud/src/features/{{name}}/actions/update-{{name}}.action.ts +5 -0
  73. package/templates/pages/crud/src/features/{{name}}/components/ProductCard.tsx +3 -0
  74. package/templates/pages/crud/src/features/{{name}}/components/ProductForm.tsx +5 -0
  75. package/templates/pages/crud/src/features/{{name}}/components/ProductsList.tsx +15 -0
  76. package/templates/pages/crud/src/features/{{name}}/index.ts +8 -0
  77. package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}.query.ts +10 -0
  78. package/templates/pages/crud/src/features/{{name}}/queries/use-{{name}}s.query.ts +10 -0
  79. package/templates/pages/crud/src/views/{{name}}/index.ts +1 -0
  80. package/templates/pages/crud/src/views/{{name}}/{{Name}}ListView.tsx +26 -0
  81. package/templates/pages/dashboard/src/app/{{name}}/layout.tsx +8 -0
  82. package/templates/pages/dashboard/src/app/{{name}}/page.tsx +5 -0
  83. package/templates/pages/dashboard/src/features/{{name}}/components/AnalyticsCard.tsx +8 -0
  84. package/templates/pages/dashboard/src/features/{{name}}/index.ts +1 -0
  85. package/templates/pages/dashboard/src/views/{{name}}/DashboardView.tsx +10 -0
  86. package/templates/pages/dashboard/src/views/{{name}}/index.ts +1 -0
  87. package/templates/pages/profile/src/app/{{name}}/page.tsx +5 -0
  88. package/templates/pages/profile/src/features/{{name}}/components/ProfileCard.tsx +8 -0
  89. package/templates/pages/profile/src/features/{{name}}/index.ts +1 -0
  90. package/templates/pages/profile/src/views/{{name}}/ProfileView.tsx +9 -0
  91. package/templates/pages/profile/src/views/{{name}}/index.ts +1 -0
  92. package/templates/pages/settings/src/app/{{name}}/page.tsx +5 -0
  93. package/templates/pages/settings/src/features/{{name}}/components/SettingsTabs.tsx +18 -0
  94. package/templates/pages/settings/src/features/{{name}}/index.ts +1 -0
  95. package/templates/pages/settings/src/views/{{name}}/SettingsView.tsx +10 -0
  96. package/templates/pages/settings/src/views/{{name}}/index.ts +1 -0
package/dist/index.js CHANGED
@@ -2,14 +2,19 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { program } from "commander";
5
- import { cancel, log as log3, outro as outro2 } from "@clack/prompts";
5
+ import { cancel as cancel3, log as log4, outro as outro2 } from "@clack/prompts";
6
6
  import chalk from "chalk";
7
- import path5 from "path";
7
+ import path8 from "path";
8
8
 
9
9
  // src/commands/generate.ts
10
+ import { log as log2 } from "@clack/prompts";
11
+ import fs4 from "fs-extra";
12
+ import path4 from "path";
13
+
14
+ // src/commands/page.ts
10
15
  import { log } from "@clack/prompts";
11
- import fs2 from "fs-extra";
12
- import path2 from "path";
16
+ import fs3 from "fs-extra";
17
+ import path3 from "path";
13
18
 
14
19
  // src/lib/naming.ts
15
20
  function toPascalCase(value) {
@@ -26,6 +31,58 @@ function assertValidSliceName(name) {
26
31
  }
27
32
  }
28
33
 
34
+ // src/lib/page-prompts.ts
35
+ import { cancel, isCancel, select } from "@clack/prompts";
36
+
37
+ // src/lib/page-presets.ts
38
+ var PAGE_PRESETS = [
39
+ "auth",
40
+ "dashboard",
41
+ "crud",
42
+ "profile",
43
+ "settings",
44
+ "blank"
45
+ ];
46
+ var DEFAULT_PAGE_PRESET = "blank";
47
+ var PAGE_PRESET_LABELS = {
48
+ auth: "auth \u2014 login/register/logout flow",
49
+ dashboard: "dashboard \u2014 layout with sidebar + analytics",
50
+ crud: "crud \u2014 list + create/edit/delete",
51
+ profile: "profile \u2014 user profile page",
52
+ settings: "settings \u2014 tabbed settings page",
53
+ blank: "blank \u2014 minimal page structure"
54
+ };
55
+
56
+ // src/lib/page-prompts.ts
57
+ function exitOnCancel(value) {
58
+ if (isCancel(value)) {
59
+ cancel("Cancelled");
60
+ process.exit(0);
61
+ }
62
+ return value;
63
+ }
64
+ async function promptPagePreset(options) {
65
+ if (options.preset) {
66
+ if (!PAGE_PRESETS.includes(options.preset)) {
67
+ throw new Error(`Unknown preset "${options.preset}". Use: ${PAGE_PRESETS.join(", ")}`);
68
+ }
69
+ return options.preset;
70
+ }
71
+ if (options.yes) {
72
+ return DEFAULT_PAGE_PRESET;
73
+ }
74
+ return exitOnCancel(
75
+ await select({
76
+ message: "What page template do you want?",
77
+ options: PAGE_PRESETS.map((preset) => ({
78
+ value: preset,
79
+ label: PAGE_PRESET_LABELS[preset]
80
+ })),
81
+ initialValue: "blank"
82
+ })
83
+ );
84
+ }
85
+
29
86
  // src/lib/paths.ts
30
87
  import path from "path";
31
88
  import { fileURLToPath } from "url";
@@ -66,24 +123,9 @@ function resolveAppTemplateDir() {
66
123
  return appTemplate;
67
124
  }
68
125
 
69
- // src/commands/generate.ts
70
- var SLICE_TYPES = ["feature", "view", "widget", "entity"];
71
- var TARGET_DIRS = {
72
- feature: "features",
73
- view: "views",
74
- widget: "widgets",
75
- entity: "entities"
76
- };
77
- function isSliceType(value) {
78
- return SLICE_TYPES.includes(value);
79
- }
80
- function assertNextProject(cwd) {
81
- const packageJson = path2.join(cwd, "package.json");
82
- const srcDir = path2.join(cwd, "src");
83
- if (!fs2.existsSync(packageJson) || !fs2.existsSync(srcDir)) {
84
- throw new Error("Run this command from the root of a Next Architecture project.");
85
- }
86
- }
126
+ // src/lib/template.ts
127
+ import fs2 from "fs-extra";
128
+ import path2 from "path";
87
129
  async function renderTemplateDir(templateDir, targetDir, replacements) {
88
130
  const created = [];
89
131
  if (!await fs2.pathExists(templateDir)) {
@@ -108,44 +150,168 @@ async function renderTemplateDir(templateDir, targetDir, replacements) {
108
150
  content = content.replaceAll(from, to);
109
151
  }
110
152
  await fs2.writeFile(targetPath, content);
111
- created.push(path2.relative(process.cwd(), targetPath));
153
+ created.push(targetPath);
112
154
  }
113
155
  return created;
114
156
  }
115
- async function generateCommand(type, name, projectRoot = process.cwd(), options = {}) {
116
- const root = path2.resolve(projectRoot);
157
+ async function copyTemplateTree(sourceDir, targetDir) {
158
+ const created = [];
159
+ if (!await fs2.pathExists(sourceDir)) {
160
+ return created;
161
+ }
162
+ const entries = await fs2.readdir(sourceDir, { withFileTypes: true });
163
+ for (const entry of entries) {
164
+ const sourcePath = path2.join(sourceDir, entry.name);
165
+ const targetPath = path2.join(targetDir, entry.name);
166
+ if (entry.isDirectory()) {
167
+ await fs2.ensureDir(targetPath);
168
+ created.push(...await copyTemplateTree(sourcePath, targetPath));
169
+ continue;
170
+ }
171
+ await fs2.ensureDir(path2.dirname(targetPath));
172
+ await fs2.copy(sourcePath, targetPath);
173
+ created.push(targetPath);
174
+ }
175
+ return created;
176
+ }
177
+ function buildReplacements(name, pascalName, kebabName) {
178
+ return {
179
+ "{{Name}}": pascalName,
180
+ "{{name}}": kebabName,
181
+ "{{NAME}}": name.toUpperCase()
182
+ };
183
+ }
184
+
185
+ // src/commands/page.ts
186
+ function assertNextProject(cwd) {
187
+ const packageJson = path3.join(cwd, "package.json");
188
+ const srcDir = path3.join(cwd, "src");
189
+ if (!fs3.existsSync(packageJson) || !fs3.existsSync(srcDir)) {
190
+ throw new Error("Run this command from the root of a Next Architecture project.");
191
+ }
192
+ }
193
+ async function pathExistsAny(paths) {
194
+ for (const candidate of paths) {
195
+ if (await fs3.pathExists(candidate)) {
196
+ return true;
197
+ }
198
+ }
199
+ return false;
200
+ }
201
+ async function pageCommand(name, projectRoot = process.cwd(), options = {}) {
202
+ const root = path3.resolve(projectRoot);
117
203
  assertNextProject(root);
118
204
  assertValidSliceName(name);
205
+ const preset = await promptPagePreset({ yes: options.yes, preset: options.preset });
206
+ const pascalName = toPascalCase(name);
207
+ const kebabName = toKebabCase(name);
208
+ const templatesDir = resolveTemplatesDir();
209
+ const templateDir = path3.join(templatesDir, "pages", preset);
210
+ if (!await fs3.pathExists(templateDir)) {
211
+ throw new Error(`Page preset "${preset}" template not found.`);
212
+ }
213
+ const targetDir = path3.join(root, "src");
214
+ const replacements = buildReplacements(name, pascalName, kebabName);
215
+ const conflictPaths = [
216
+ path3.join(targetDir, "app", kebabName),
217
+ path3.join(targetDir, "app", `(${kebabName})`),
218
+ path3.join(targetDir, "views", kebabName),
219
+ path3.join(targetDir, "features", kebabName),
220
+ path3.join(targetDir, "entities", kebabName)
221
+ ];
222
+ if (!options.force && await pathExistsAny(conflictPaths)) {
223
+ throw new Error(
224
+ `Page "${kebabName}" already exists. Use --force to overwrite conflicting paths.`
225
+ );
226
+ }
227
+ const previousCwd = process.cwd();
228
+ process.chdir(root);
229
+ try {
230
+ if (options.force) {
231
+ for (const conflictPath of conflictPaths) {
232
+ if (await fs3.pathExists(conflictPath)) {
233
+ await fs3.remove(conflictPath);
234
+ }
235
+ }
236
+ }
237
+ const created = await renderTemplateDir(templateDir, root, replacements);
238
+ const relativeFiles = created.map((file) => path3.relative(root, file));
239
+ log.success(`Created ${preset} page "${kebabName}"`);
240
+ for (const file of relativeFiles) {
241
+ log.info(` ${file}`);
242
+ }
243
+ if (preset === "blank") {
244
+ log.info(`Route: src/app/${kebabName}/page.tsx`);
245
+ }
246
+ if (preset === "auth") {
247
+ log.info(`Routes: src/app/(${kebabName})/login and register`);
248
+ }
249
+ if (preset === "crud") {
250
+ log.info(`Routes: src/app/${kebabName}, ${kebabName}/[id], ${kebabName}/new`);
251
+ }
252
+ } finally {
253
+ process.chdir(previousCwd);
254
+ }
255
+ }
256
+
257
+ // src/commands/generate.ts
258
+ var SLICE_TYPES = ["feature", "view", "widget", "entity"];
259
+ var TARGET_DIRS = {
260
+ feature: "features",
261
+ view: "views",
262
+ widget: "widgets",
263
+ entity: "entities"
264
+ };
265
+ function isSliceType(value) {
266
+ return SLICE_TYPES.includes(value);
267
+ }
268
+ function assertNextProject2(cwd) {
269
+ const packageJson = path4.join(cwd, "package.json");
270
+ const srcDir = path4.join(cwd, "src");
271
+ if (!fs4.existsSync(packageJson) || !fs4.existsSync(srcDir)) {
272
+ throw new Error("Run this command from the root of a Next Architecture project.");
273
+ }
274
+ }
275
+ async function generateCommand(type, name, projectRoot = process.cwd(), options = {}) {
276
+ if (type === "page") {
277
+ await pageCommand(name, projectRoot, {
278
+ force: options.force,
279
+ yes: options.yes,
280
+ preset: options.preset
281
+ });
282
+ return;
283
+ }
284
+ const root = path4.resolve(projectRoot);
285
+ assertNextProject2(root);
286
+ assertValidSliceName(name);
119
287
  if (!isSliceType(type)) {
120
- throw new Error(`Unknown type "${type}". Use: ${SLICE_TYPES.join(", ")}`);
288
+ throw new Error(`Unknown type "${type}". Use: page, ${SLICE_TYPES.join(", ")}`);
121
289
  }
122
290
  const pascalName = toPascalCase(name);
123
291
  const kebabName = toKebabCase(name);
124
292
  const templatesDir = resolveTemplatesDir();
125
- const templateDir = path2.join(templatesDir, type);
126
- const targetDir = path2.join(root, "src", TARGET_DIRS[type], kebabName);
293
+ const templateDir = path4.join(templatesDir, type);
294
+ const targetDir = path4.join(root, "src", TARGET_DIRS[type], kebabName);
127
295
  const previousCwd = process.cwd();
128
296
  process.chdir(root);
129
297
  try {
130
- if (await fs2.pathExists(targetDir)) {
298
+ if (await fs4.pathExists(targetDir)) {
131
299
  if (!options.force) {
132
300
  throw new Error(
133
301
  `"${type}" "${kebabName}" already exists at ${targetDir}. Use --force to overwrite.`
134
302
  );
135
303
  }
136
- await fs2.remove(targetDir);
304
+ await fs4.remove(targetDir);
137
305
  }
138
- const replacements = {
139
- "{{Name}}": pascalName,
140
- "{{name}}": kebabName
141
- };
306
+ const replacements = buildReplacements(name, pascalName, kebabName);
142
307
  const created = await renderTemplateDir(templateDir, targetDir, replacements);
143
- log.success(`Created ${type} "${kebabName}" in ${root}`);
144
- for (const file of created) {
145
- log.info(` ${file}`);
308
+ const relativeFiles = created.map((file) => path4.relative(root, file));
309
+ log2.success(`Created ${type} "${kebabName}" in ${root}`);
310
+ for (const file of relativeFiles) {
311
+ log2.info(` ${file}`);
146
312
  }
147
313
  if (type === "view") {
148
- log.info(`Add to a route: import { ${pascalName} } from '@/views/${kebabName}';`);
314
+ log2.info(`Add to a route: import { ${pascalName} } from '@/views/${kebabName}';`);
149
315
  }
150
316
  } finally {
151
317
  process.chdir(previousCwd);
@@ -153,33 +319,324 @@ async function generateCommand(type, name, projectRoot = process.cwd(), options
153
319
  }
154
320
 
155
321
  // src/commands/init.ts
156
- import { confirm, intro, log as log2, outro } from "@clack/prompts";
157
- import fs4 from "fs-extra";
158
- import path4 from "path";
322
+ import { confirm as confirm2, intro, log as log3, outro } from "@clack/prompts";
323
+ import fs7 from "fs-extra";
324
+ import path7 from "path";
325
+
326
+ // src/lib/apply-packages.ts
327
+ import fs5 from "fs-extra";
328
+ import path5 from "path";
329
+
330
+ // src/lib/packages.ts
331
+ var DEFAULT_INIT_SELECTIONS = {
332
+ stateManager: "zustand",
333
+ formLibrary: "tanstack-form",
334
+ optionalPackages: ["tanstack-table"],
335
+ withExamples: true
336
+ };
337
+ var PACKAGE_VERSIONS = {
338
+ zustand: "^5.0.14",
339
+ "@reduxjs/toolkit": "^2.8.2",
340
+ "react-redux": "^9.2.0",
341
+ jotai: "^2.12.5",
342
+ "@tanstack/react-form": "^1.0.0",
343
+ zod: "^4.4.3",
344
+ "react-hook-form": "^7.56.4",
345
+ "@hookform/resolvers": "^5.0.1",
346
+ "@tanstack/react-query": "^5.101.2",
347
+ "@tanstack/react-query-devtools": "^5.101.2",
348
+ "@tanstack/react-table": "^8.21.3",
349
+ motion: "^12.19.1",
350
+ nuqs: "^2.4.3",
351
+ "@trpc/client": "^11.1.2",
352
+ "@trpc/server": "^11.1.2",
353
+ "@trpc/react-query": "^11.1.2",
354
+ "better-auth": "^1.2.8",
355
+ uploadthing: "^7.7.2",
356
+ "@uploadthing/react": "^7.3.1",
357
+ sonner: "^2.0.3",
358
+ "next-intl": "^4.1.0",
359
+ "@sentry/nextjs": "^9.22.0"
360
+ };
361
+ function resolveDependencies(selections) {
362
+ const dependencies = {};
363
+ const devDependencies = {};
364
+ dependencies["@tanstack/react-query"] = PACKAGE_VERSIONS["@tanstack/react-query"];
365
+ devDependencies["@tanstack/react-query-devtools"] = PACKAGE_VERSIONS["@tanstack/react-query-devtools"];
366
+ dependencies.zod = PACKAGE_VERSIONS.zod;
367
+ switch (selections.stateManager) {
368
+ case "zustand":
369
+ dependencies.zustand = PACKAGE_VERSIONS.zustand;
370
+ break;
371
+ case "redux":
372
+ dependencies["@reduxjs/toolkit"] = PACKAGE_VERSIONS["@reduxjs/toolkit"];
373
+ dependencies["react-redux"] = PACKAGE_VERSIONS["react-redux"];
374
+ break;
375
+ case "jotai":
376
+ dependencies.jotai = PACKAGE_VERSIONS.jotai;
377
+ break;
378
+ case "none":
379
+ break;
380
+ }
381
+ switch (selections.formLibrary) {
382
+ case "tanstack-form":
383
+ dependencies["@tanstack/react-form"] = PACKAGE_VERSIONS["@tanstack/react-form"];
384
+ break;
385
+ case "react-hook-form":
386
+ dependencies["react-hook-form"] = PACKAGE_VERSIONS["react-hook-form"];
387
+ dependencies["@hookform/resolvers"] = PACKAGE_VERSIONS["@hookform/resolvers"];
388
+ break;
389
+ case "none":
390
+ break;
391
+ }
392
+ for (const pkg of selections.optionalPackages) {
393
+ switch (pkg) {
394
+ case "tanstack-table":
395
+ dependencies["@tanstack/react-table"] = PACKAGE_VERSIONS["@tanstack/react-table"];
396
+ break;
397
+ case "motion":
398
+ dependencies.motion = PACKAGE_VERSIONS.motion;
399
+ break;
400
+ case "nuqs":
401
+ dependencies.nuqs = PACKAGE_VERSIONS.nuqs;
402
+ break;
403
+ case "trpc":
404
+ dependencies["@trpc/client"] = PACKAGE_VERSIONS["@trpc/client"];
405
+ dependencies["@trpc/server"] = PACKAGE_VERSIONS["@trpc/server"];
406
+ dependencies["@trpc/react-query"] = PACKAGE_VERSIONS["@trpc/react-query"];
407
+ break;
408
+ case "better-auth":
409
+ dependencies["better-auth"] = PACKAGE_VERSIONS["better-auth"];
410
+ break;
411
+ case "uploadthing":
412
+ dependencies.uploadthing = PACKAGE_VERSIONS.uploadthing;
413
+ dependencies["@uploadthing/react"] = PACKAGE_VERSIONS["@uploadthing/react"];
414
+ break;
415
+ case "sonner":
416
+ dependencies.sonner = PACKAGE_VERSIONS.sonner;
417
+ break;
418
+ case "next-intl":
419
+ dependencies["next-intl"] = PACKAGE_VERSIONS["next-intl"];
420
+ break;
421
+ case "sentry":
422
+ dependencies["@sentry/nextjs"] = PACKAGE_VERSIONS["@sentry/nextjs"];
423
+ break;
424
+ }
425
+ }
426
+ return { dependencies, devDependencies };
427
+ }
428
+ function getPackageTemplates(selections) {
429
+ const templates = /* @__PURE__ */ new Set(["tanstack-query", "env"]);
430
+ switch (selections.stateManager) {
431
+ case "zustand":
432
+ templates.add("zustand");
433
+ break;
434
+ case "redux":
435
+ templates.add("redux");
436
+ break;
437
+ case "jotai":
438
+ templates.add("jotai");
439
+ break;
440
+ case "none":
441
+ break;
442
+ }
443
+ switch (selections.formLibrary) {
444
+ case "tanstack-form":
445
+ templates.add("tanstack-form");
446
+ break;
447
+ case "react-hook-form":
448
+ templates.add("react-hook-form");
449
+ break;
450
+ case "none":
451
+ break;
452
+ }
453
+ for (const pkg of selections.optionalPackages) {
454
+ templates.add(pkg);
455
+ }
456
+ if (selections.optionalPackages.includes("sonner")) {
457
+ templates.add("sonner-provider");
458
+ }
459
+ return [...templates];
460
+ }
461
+ function formatSelectionsSummary(selections) {
462
+ const lines = [];
463
+ const stateLabels = {
464
+ zustand: "Zustand",
465
+ redux: "Redux Toolkit",
466
+ jotai: "Jotai",
467
+ none: "None"
468
+ };
469
+ const formLabels = {
470
+ "tanstack-form": "TanStack Form + zod",
471
+ "react-hook-form": "React Hook Form + zod",
472
+ none: "None"
473
+ };
474
+ lines.push(`State: ${stateLabels[selections.stateManager]}`);
475
+ lines.push(`Forms: ${formLabels[selections.formLibrary]}`);
476
+ lines.push("Always: TanStack Query + Devtools");
477
+ const optionalLabels = {
478
+ "tanstack-table": "TanStack Table",
479
+ motion: "Motion",
480
+ nuqs: "nuqs",
481
+ trpc: "tRPC",
482
+ "better-auth": "Better Auth",
483
+ uploadthing: "Uploadthing",
484
+ sonner: "Sonner",
485
+ "next-intl": "next-intl",
486
+ "sentry": "Sentry"
487
+ };
488
+ if (selections.optionalPackages.length > 0) {
489
+ lines.push(
490
+ `Optional: ${selections.optionalPackages.map((p) => optionalLabels[p]).join(", ")}`
491
+ );
492
+ } else {
493
+ lines.push("Optional: none");
494
+ }
495
+ lines.push(`Examples: ${selections.withExamples ? "yes" : "no"}`);
496
+ return lines;
497
+ }
498
+
499
+ // src/lib/apply-packages.ts
500
+ async function mergePackageJson(targetDir, selections) {
501
+ const packageJsonPath = path5.join(targetDir, "package.json");
502
+ const pkg = JSON.parse(await fs5.readFile(packageJsonPath, "utf8"));
503
+ const { dependencies, devDependencies } = resolveDependencies(selections);
504
+ pkg.dependencies = { ...pkg.dependencies, ...dependencies };
505
+ pkg.devDependencies = { ...pkg.devDependencies, ...devDependencies };
506
+ await fs5.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
507
+ `);
508
+ }
509
+ async function applyPackageSelections(targetDir, selections) {
510
+ const packagesDir = path5.join(resolveTemplatesDir(), "packages");
511
+ const created = [];
512
+ await mergePackageJson(targetDir, selections);
513
+ for (const templateId of getPackageTemplates(selections)) {
514
+ const templateRoot = path5.join(packagesDir, templateId);
515
+ const coreSrc = path5.join(templateRoot, "core", "src");
516
+ const examplesSrc = path5.join(templateRoot, "examples", "src");
517
+ if (await fs5.pathExists(coreSrc)) {
518
+ const files = await copyTemplateTree(coreSrc, path5.join(targetDir, "src"));
519
+ created.push(...files);
520
+ }
521
+ if (selections.withExamples && await fs5.pathExists(examplesSrc)) {
522
+ const files = await copyTemplateTree(examplesSrc, path5.join(targetDir, "src"));
523
+ created.push(...files);
524
+ }
525
+ }
526
+ return created;
527
+ }
159
528
 
160
529
  // src/lib/copy.ts
161
- import fs3 from "fs-extra";
162
- import path3 from "path";
530
+ import fs6 from "fs-extra";
531
+ import path6 from "path";
163
532
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".turbo", "dist"]);
164
533
  async function copyProjectTemplate(sourceDir, targetDir) {
165
- await fs3.copy(sourceDir, targetDir, {
534
+ await fs6.copy(sourceDir, targetDir, {
166
535
  filter(src) {
167
- const relative = path3.relative(sourceDir, src);
536
+ const relative = path6.relative(sourceDir, src);
168
537
  if (!relative) return true;
169
- return !relative.split(path3.sep).some((part) => EXCLUDED_DIRS.has(part));
538
+ return !relative.split(path6.sep).some((part) => EXCLUDED_DIRS.has(part));
170
539
  }
171
540
  });
172
541
  }
173
542
 
543
+ // src/lib/init-prompts.ts
544
+ import { cancel as cancel2, confirm, isCancel as isCancel2, multiselect, select as select2 } from "@clack/prompts";
545
+ function exitOnCancel2(value) {
546
+ if (isCancel2(value)) {
547
+ cancel2("Cancelled");
548
+ process.exit(0);
549
+ }
550
+ return value;
551
+ }
552
+ async function promptInitSelections(options = {}) {
553
+ if (options.yes) {
554
+ return {
555
+ ...DEFAULT_INIT_SELECTIONS,
556
+ withExamples: options.noExamples ? false : DEFAULT_INIT_SELECTIONS.withExamples
557
+ };
558
+ }
559
+ const stateManager = exitOnCancel2(
560
+ await select2({
561
+ message: "Which state manager do you want?",
562
+ options: [
563
+ { value: "zustand", label: "Zustand (recommended)" },
564
+ { value: "redux", label: "Redux Toolkit" },
565
+ { value: "jotai", label: "Jotai" },
566
+ { value: "none", label: "None" }
567
+ ],
568
+ initialValue: "zustand"
569
+ })
570
+ );
571
+ const formLibrary = exitOnCancel2(
572
+ await select2({
573
+ message: "Which form library do you want?",
574
+ options: [
575
+ { value: "tanstack-form", label: "TanStack Form (recommended)" },
576
+ { value: "react-hook-form", label: "React Hook Form" },
577
+ { value: "none", label: "None" }
578
+ ],
579
+ initialValue: "tanstack-form"
580
+ })
581
+ );
582
+ const optionalPackages = exitOnCancel2(
583
+ await multiselect({
584
+ message: "Select additional packages (multi-select)",
585
+ options: [
586
+ { value: "tanstack-table", label: "TanStack Table (headless tables)" },
587
+ { value: "motion", label: "Motion / Framer Motion (animations)" },
588
+ { value: "nuqs", label: "nuqs (URL state management)" },
589
+ { value: "trpc", label: "tRPC (end-to-end type safety, if using separate backend)" },
590
+ { value: "better-auth", label: "Better Auth (alternative to NextAuth)" },
591
+ { value: "uploadthing", label: "Uploadthing (file uploads)" },
592
+ { value: "sonner", label: "Sonner (toast notifications)" },
593
+ { value: "next-intl", label: "next-intl (i18n)" },
594
+ { value: "sentry", label: "Sentry (error tracking)" }
595
+ ],
596
+ initialValues: ["tanstack-table"],
597
+ required: false
598
+ })
599
+ );
600
+ let withExamples = true;
601
+ if (!options.noExamples) {
602
+ withExamples = exitOnCancel2(
603
+ await confirm({
604
+ message: "Generate example files with comments (Russian)?",
605
+ initialValue: true
606
+ })
607
+ );
608
+ } else {
609
+ withExamples = false;
610
+ }
611
+ const selections = {
612
+ stateManager,
613
+ formLibrary,
614
+ optionalPackages,
615
+ withExamples
616
+ };
617
+ const proceed = exitOnCancel2(
618
+ await confirm({
619
+ message: `Proceed with this setup?
620
+ ${formatSelectionsSummary(selections).map((l) => ` \u2022 ${l}`).join("\n")}`,
621
+ initialValue: true
622
+ })
623
+ );
624
+ if (!proceed) {
625
+ cancel2("Cancelled");
626
+ process.exit(0);
627
+ }
628
+ return selections;
629
+ }
630
+
174
631
  // src/commands/init.ts
175
632
  async function resolveEslintPluginSource() {
176
633
  const packageRoot = getPackageRoot();
177
634
  const candidates = [
178
- path4.join(packageRoot, "vendor", "eslint-plugin-next-arch"),
179
- path4.resolve(packageRoot, "..", "..", "packages", "eslint-plugin-next-arch")
635
+ path7.join(packageRoot, "vendor", "eslint-plugin-next-arch"),
636
+ path7.resolve(packageRoot, "..", "..", "packages", "eslint-plugin-next-arch")
180
637
  ];
181
638
  for (const candidate of candidates) {
182
- if (await fs4.pathExists(path4.join(candidate, "dist", "index.js"))) {
639
+ if (await fs7.pathExists(path7.join(candidate, "dist", "index.js"))) {
183
640
  return candidate;
184
641
  }
185
642
  }
@@ -189,29 +646,33 @@ async function resolveEslintPluginSource() {
189
646
  }
190
647
  async function bundleEslintPlugin(targetDir) {
191
648
  const pluginSource = await resolveEslintPluginSource();
192
- const pluginTarget = path4.join(targetDir, "vendor", "eslint-plugin-next-arch");
193
- await fs4.ensureDir(pluginTarget);
194
- await fs4.copy(path4.join(pluginSource, "dist"), path4.join(pluginTarget, "dist"));
195
- await fs4.copy(path4.join(pluginSource, "package.json"), path4.join(pluginTarget, "package.json"));
649
+ const pluginTarget = path7.join(targetDir, "vendor", "eslint-plugin-next-arch");
650
+ await fs7.ensureDir(pluginTarget);
651
+ await fs7.copy(path7.join(pluginSource, "dist"), path7.join(pluginTarget, "dist"));
652
+ await fs7.copy(path7.join(pluginSource, "package.json"), path7.join(pluginTarget, "package.json"));
196
653
  }
197
654
  async function patchPackageJson(targetDir, projectName) {
198
- const packageJsonPath = path4.join(targetDir, "package.json");
199
- const pkg = JSON.parse(await fs4.readFile(packageJsonPath, "utf8"));
655
+ const packageJsonPath = path7.join(targetDir, "package.json");
656
+ const pkg = JSON.parse(await fs7.readFile(packageJsonPath, "utf8"));
200
657
  pkg.name = projectName;
201
658
  delete pkg.scripts?.arch;
202
659
  if (pkg.devDependencies) {
203
660
  pkg.devDependencies["eslint-plugin-next-arch"] = "file:./vendor/eslint-plugin-next-arch";
204
661
  }
205
- await fs4.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
662
+ await fs7.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
206
663
  `);
207
664
  }
208
665
  async function initCommand(projectName, options = {}) {
209
- const baseDir = path4.resolve(options.cwd ?? process.cwd());
666
+ const baseDir = path7.resolve(options.cwd ?? process.cwd());
210
667
  intro("Creating new Next Architecture project...");
211
- const targetDir = path4.join(baseDir, projectName);
668
+ const selections = await promptInitSelections({
669
+ yes: options.yes,
670
+ noExamples: options.noExamples
671
+ });
672
+ const targetDir = path7.join(baseDir, projectName);
212
673
  const templateDir = resolveAppTemplateDir();
213
- if (await fs4.pathExists(targetDir)) {
214
- const shouldContinue = await confirm({
674
+ if (await fs7.pathExists(targetDir)) {
675
+ const shouldContinue = await confirm2({
215
676
  message: "Directory already exists. Continue and merge files?"
216
677
  });
217
678
  if (!shouldContinue) {
@@ -219,41 +680,77 @@ async function initCommand(projectName, options = {}) {
219
680
  return;
220
681
  }
221
682
  }
222
- log2.info(`Copying template from ${path4.basename(templateDir)}...`);
683
+ log3.info("Package setup:");
684
+ for (const line of formatSelectionsSummary(selections)) {
685
+ log3.info(` ${line}`);
686
+ }
687
+ log3.info(`Copying template from ${path7.basename(templateDir)}...`);
223
688
  await copyProjectTemplate(templateDir, targetDir);
224
689
  await bundleEslintPlugin(targetDir);
225
690
  await patchPackageJson(targetDir, projectName);
226
- await fs4.writeFile(path4.join(targetDir, ".npmrc"), "ignore-workspace=true\n");
227
- log2.success(`Project "${projectName}" created`);
228
- log2.info(` cd ${projectName}`);
229
- log2.info(" pnpm install");
230
- log2.info(" pnpm dev");
691
+ await applyPackageSelections(targetDir, selections);
692
+ await fs7.writeFile(path7.join(targetDir, ".npmrc"), "ignore-workspace=true\n");
693
+ log3.success(`Project "${projectName}" created`);
694
+ log3.info(` cd ${projectName}`);
695
+ log3.info(" npm install");
696
+ log3.info(" npm run dev");
697
+ if (selections.withExamples) {
698
+ log3.info(" See src/features/_examples/ for commented package examples");
699
+ }
231
700
  outro("Done!");
232
701
  }
233
702
 
234
703
  // src/index.ts
235
704
  console.log(chalk.blue("Next Architecture CLI"));
236
- program.name("next-arch").description("CLI for Next.js Feature-Sliced Architecture").version("0.1.1");
237
- program.command("init <projectName>").description("Create a new project with Next Architecture").option("-C, --cwd <path>", "directory where the project folder will be created").action(async (projectName, options) => {
238
- try {
239
- await initCommand(projectName, { cwd: options.cwd });
240
- } catch (error) {
241
- cancel(error instanceof Error ? error.message : "Init failed");
242
- process.exit(1);
705
+ program.name("next-arch").description("CLI for Next.js Feature-Sliced Architecture").version("0.2.0");
706
+ program.command("init <projectName>").description("Create a new project with Next Architecture").option("-C, --cwd <path>", "directory where the project folder will be created").option("-y, --yes", "use default package selections without prompts").option("--no-examples", "skip generating example files").action(
707
+ async (projectName, options) => {
708
+ try {
709
+ await initCommand(projectName, {
710
+ cwd: options.cwd,
711
+ yes: options.yes,
712
+ noExamples: options.examples === false
713
+ });
714
+ } catch (error) {
715
+ cancel3(error instanceof Error ? error.message : "Init failed");
716
+ process.exit(1);
717
+ }
243
718
  }
244
- });
245
- program.command("generate <type> <name>").alias("g").description("Generate feature, widget, entity, or view").option("-C, --cwd <path>", "path to Next.js project root (default: current directory)").option("-f, --force", "overwrite existing slice").action(async (type, name, options) => {
246
- try {
247
- const projectRoot = options.cwd ? path5.resolve(options.cwd) : process.cwd();
248
- await generateCommand(type, name, projectRoot, { force: options.force });
249
- outro2("Done!");
250
- } catch (error) {
251
- cancel(error instanceof Error ? error.message : "Generate failed");
252
- process.exit(1);
719
+ );
720
+ program.command("page <name>").description("Generate a full FSD page (view + feature + routes)").option("-C, --cwd <path>", "path to Next.js project root (default: current directory)").option("-f, --force", "overwrite existing page paths").option("-y, --yes", "use blank preset without prompts").option("--preset <preset>", "page preset: auth, dashboard, crud, profile, settings, blank").action(
721
+ async (name, options) => {
722
+ try {
723
+ const projectRoot = options.cwd ? path8.resolve(options.cwd) : process.cwd();
724
+ await pageCommand(name, projectRoot, {
725
+ force: options.force,
726
+ yes: options.yes,
727
+ preset: options.preset
728
+ });
729
+ outro2("Done!");
730
+ } catch (error) {
731
+ cancel3(error instanceof Error ? error.message : "Page generation failed");
732
+ process.exit(1);
733
+ }
253
734
  }
254
- });
735
+ );
736
+ program.command("generate <type> <name>").alias("g").description("Generate page, feature, widget, entity, or view").option("-C, --cwd <path>", "path to Next.js project root (default: current directory)").option("-f, --force", "overwrite existing slice").option("-y, --yes", "skip interactive page preset selection").option("--preset <preset>", "page preset when type is page").action(
737
+ async (type, name, options) => {
738
+ try {
739
+ const projectRoot = options.cwd ? path8.resolve(options.cwd) : process.cwd();
740
+ await generateCommand(type, name, projectRoot, {
741
+ force: options.force,
742
+ yes: options.yes,
743
+ preset: options.preset
744
+ });
745
+ outro2("Done!");
746
+ } catch (error) {
747
+ cancel3(error instanceof Error ? error.message : "Generate failed");
748
+ process.exit(1);
749
+ }
750
+ }
751
+ );
255
752
  program.parseAsync(process.argv).catch((error) => {
256
- log3.error(error instanceof Error ? error.message : "Unexpected error");
753
+ log4.error(error instanceof Error ? error.message : "Unexpected error");
257
754
  process.exit(1);
258
755
  });
259
756
  //# sourceMappingURL=index.js.map