@yousxlfs/next-arch 0.1.0 → 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 +586 -85
  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,17 +31,73 @@ 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";
32
89
  import fs from "fs-extra";
90
+ var PACKAGE_NAMES = /* @__PURE__ */ new Set(["next-arch", "@yousxlfs/next-arch"]);
91
+ function isNextArchPackage(pkg) {
92
+ return typeof pkg.name === "string" && PACKAGE_NAMES.has(pkg.name);
93
+ }
33
94
  function findPackageRoot(startDir) {
34
95
  let current = startDir;
35
96
  while (current !== path.dirname(current)) {
36
97
  const packageJsonPath = path.join(current, "package.json");
37
98
  if (fs.existsSync(packageJsonPath)) {
38
99
  const pkg = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
39
- if (pkg.name === "next-arch") {
100
+ if (isNextArchPackage(pkg)) {
40
101
  return current;
41
102
  }
42
103
  }
@@ -62,24 +123,9 @@ function resolveAppTemplateDir() {
62
123
  return appTemplate;
63
124
  }
64
125
 
65
- // src/commands/generate.ts
66
- var SLICE_TYPES = ["feature", "view", "widget", "entity"];
67
- var TARGET_DIRS = {
68
- feature: "features",
69
- view: "views",
70
- widget: "widgets",
71
- entity: "entities"
72
- };
73
- function isSliceType(value) {
74
- return SLICE_TYPES.includes(value);
75
- }
76
- function assertNextProject(cwd) {
77
- const packageJson = path2.join(cwd, "package.json");
78
- const srcDir = path2.join(cwd, "src");
79
- if (!fs2.existsSync(packageJson) || !fs2.existsSync(srcDir)) {
80
- throw new Error("Run this command from the root of a Next Architecture project.");
81
- }
82
- }
126
+ // src/lib/template.ts
127
+ import fs2 from "fs-extra";
128
+ import path2 from "path";
83
129
  async function renderTemplateDir(templateDir, targetDir, replacements) {
84
130
  const created = [];
85
131
  if (!await fs2.pathExists(templateDir)) {
@@ -104,44 +150,168 @@ async function renderTemplateDir(templateDir, targetDir, replacements) {
104
150
  content = content.replaceAll(from, to);
105
151
  }
106
152
  await fs2.writeFile(targetPath, content);
107
- created.push(path2.relative(process.cwd(), targetPath));
153
+ created.push(targetPath);
108
154
  }
109
155
  return created;
110
156
  }
111
- async function generateCommand(type, name, projectRoot = process.cwd(), options = {}) {
112
- 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);
113
203
  assertNextProject(root);
114
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);
115
287
  if (!isSliceType(type)) {
116
- throw new Error(`Unknown type "${type}". Use: ${SLICE_TYPES.join(", ")}`);
288
+ throw new Error(`Unknown type "${type}". Use: page, ${SLICE_TYPES.join(", ")}`);
117
289
  }
118
290
  const pascalName = toPascalCase(name);
119
291
  const kebabName = toKebabCase(name);
120
292
  const templatesDir = resolveTemplatesDir();
121
- const templateDir = path2.join(templatesDir, type);
122
- 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);
123
295
  const previousCwd = process.cwd();
124
296
  process.chdir(root);
125
297
  try {
126
- if (await fs2.pathExists(targetDir)) {
298
+ if (await fs4.pathExists(targetDir)) {
127
299
  if (!options.force) {
128
300
  throw new Error(
129
301
  `"${type}" "${kebabName}" already exists at ${targetDir}. Use --force to overwrite.`
130
302
  );
131
303
  }
132
- await fs2.remove(targetDir);
304
+ await fs4.remove(targetDir);
133
305
  }
134
- const replacements = {
135
- "{{Name}}": pascalName,
136
- "{{name}}": kebabName
137
- };
306
+ const replacements = buildReplacements(name, pascalName, kebabName);
138
307
  const created = await renderTemplateDir(templateDir, targetDir, replacements);
139
- log.success(`Created ${type} "${kebabName}" in ${root}`);
140
- for (const file of created) {
141
- 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}`);
142
312
  }
143
313
  if (type === "view") {
144
- log.info(`Add to a route: import { ${pascalName} } from '@/views/${kebabName}';`);
314
+ log2.info(`Add to a route: import { ${pascalName} } from '@/views/${kebabName}';`);
145
315
  }
146
316
  } finally {
147
317
  process.chdir(previousCwd);
@@ -149,33 +319,324 @@ async function generateCommand(type, name, projectRoot = process.cwd(), options
149
319
  }
150
320
 
151
321
  // src/commands/init.ts
152
- import { confirm, intro, log as log2, outro } from "@clack/prompts";
153
- import fs4 from "fs-extra";
154
- 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
+ }
155
528
 
156
529
  // src/lib/copy.ts
157
- import fs3 from "fs-extra";
158
- import path3 from "path";
530
+ import fs6 from "fs-extra";
531
+ import path6 from "path";
159
532
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["node_modules", ".next", ".turbo", "dist"]);
160
533
  async function copyProjectTemplate(sourceDir, targetDir) {
161
- await fs3.copy(sourceDir, targetDir, {
534
+ await fs6.copy(sourceDir, targetDir, {
162
535
  filter(src) {
163
- const relative = path3.relative(sourceDir, src);
536
+ const relative = path6.relative(sourceDir, src);
164
537
  if (!relative) return true;
165
- return !relative.split(path3.sep).some((part) => EXCLUDED_DIRS.has(part));
538
+ return !relative.split(path6.sep).some((part) => EXCLUDED_DIRS.has(part));
166
539
  }
167
540
  });
168
541
  }
169
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
+
170
631
  // src/commands/init.ts
171
632
  async function resolveEslintPluginSource() {
172
633
  const packageRoot = getPackageRoot();
173
634
  const candidates = [
174
- path4.join(packageRoot, "vendor", "eslint-plugin-next-arch"),
175
- 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")
176
637
  ];
177
638
  for (const candidate of candidates) {
178
- if (await fs4.pathExists(path4.join(candidate, "dist", "index.js"))) {
639
+ if (await fs7.pathExists(path7.join(candidate, "dist", "index.js"))) {
179
640
  return candidate;
180
641
  }
181
642
  }
@@ -185,29 +646,33 @@ async function resolveEslintPluginSource() {
185
646
  }
186
647
  async function bundleEslintPlugin(targetDir) {
187
648
  const pluginSource = await resolveEslintPluginSource();
188
- const pluginTarget = path4.join(targetDir, "vendor", "eslint-plugin-next-arch");
189
- await fs4.ensureDir(pluginTarget);
190
- await fs4.copy(path4.join(pluginSource, "dist"), path4.join(pluginTarget, "dist"));
191
- 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"));
192
653
  }
193
654
  async function patchPackageJson(targetDir, projectName) {
194
- const packageJsonPath = path4.join(targetDir, "package.json");
195
- 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"));
196
657
  pkg.name = projectName;
197
658
  delete pkg.scripts?.arch;
198
659
  if (pkg.devDependencies) {
199
660
  pkg.devDependencies["eslint-plugin-next-arch"] = "file:./vendor/eslint-plugin-next-arch";
200
661
  }
201
- await fs4.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
662
+ await fs7.writeFile(packageJsonPath, `${JSON.stringify(pkg, null, 2)}
202
663
  `);
203
664
  }
204
665
  async function initCommand(projectName, options = {}) {
205
- const baseDir = path4.resolve(options.cwd ?? process.cwd());
666
+ const baseDir = path7.resolve(options.cwd ?? process.cwd());
206
667
  intro("Creating new Next Architecture project...");
207
- 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);
208
673
  const templateDir = resolveAppTemplateDir();
209
- if (await fs4.pathExists(targetDir)) {
210
- const shouldContinue = await confirm({
674
+ if (await fs7.pathExists(targetDir)) {
675
+ const shouldContinue = await confirm2({
211
676
  message: "Directory already exists. Continue and merge files?"
212
677
  });
213
678
  if (!shouldContinue) {
@@ -215,41 +680,77 @@ async function initCommand(projectName, options = {}) {
215
680
  return;
216
681
  }
217
682
  }
218
- 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)}...`);
219
688
  await copyProjectTemplate(templateDir, targetDir);
220
689
  await bundleEslintPlugin(targetDir);
221
690
  await patchPackageJson(targetDir, projectName);
222
- await fs4.writeFile(path4.join(targetDir, ".npmrc"), "ignore-workspace=true\n");
223
- log2.success(`Project "${projectName}" created`);
224
- log2.info(` cd ${projectName}`);
225
- log2.info(" pnpm install");
226
- 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
+ }
227
700
  outro("Done!");
228
701
  }
229
702
 
230
703
  // src/index.ts
231
704
  console.log(chalk.blue("Next Architecture CLI"));
232
- program.name("next-arch").description("CLI for Next.js Feature-Sliced Architecture").version("0.1.0");
233
- 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) => {
234
- try {
235
- await initCommand(projectName, { cwd: options.cwd });
236
- } catch (error) {
237
- cancel(error instanceof Error ? error.message : "Init failed");
238
- 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
+ }
239
718
  }
240
- });
241
- 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) => {
242
- try {
243
- const projectRoot = options.cwd ? path5.resolve(options.cwd) : process.cwd();
244
- await generateCommand(type, name, projectRoot, { force: options.force });
245
- outro2("Done!");
246
- } catch (error) {
247
- cancel(error instanceof Error ? error.message : "Generate failed");
248
- 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
+ }
249
734
  }
250
- });
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
+ );
251
752
  program.parseAsync(process.argv).catch((error) => {
252
- log3.error(error instanceof Error ? error.message : "Unexpected error");
753
+ log4.error(error instanceof Error ? error.message : "Unexpected error");
253
754
  process.exit(1);
254
755
  });
255
756
  //# sourceMappingURL=index.js.map