create-stackflow 1.0.5 → 1.0.7

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.
@@ -3,6 +3,7 @@ import fs from "fs-extra";
3
3
  import ora from "ora";
4
4
  import { execa } from "execa";
5
5
  import { jsxExt, writeTemplate } from "../utils/template.js";
6
+ import { chatFrontendFiles, chatNextPages } from "./chat.js";
6
7
 
7
8
  export async function createFrontend(context) {
8
9
  await scaffoldOfficialFrontend(context);
@@ -43,30 +44,6 @@ async function scaffoldOfficialFrontend(context) {
43
44
 
44
45
  async function installFrontendDependencies(context) {
45
46
  await updateFrontendPackage(context);
46
- if (context.skipInstall) return;
47
- const spinner = ora("Installing frontend dependencies").start();
48
- try {
49
- const deps = [
50
- "axios@latest",
51
- "sonner@latest",
52
- "lucide-react@latest",
53
- "clsx@latest",
54
- "tailwind-merge@latest"
55
- ];
56
- if (context.frontend === "react" && context.router) deps.push("react-router-dom@latest");
57
- if (context.state === "zustand") deps.push("zustand@latest");
58
- if (context.state === "redux-toolkit") deps.push("@reduxjs/toolkit@latest", "react-redux@latest");
59
- if (context.form && context.form !== "none") deps.push("react-hook-form@latest", "@hookform/resolvers@latest");
60
- if (context.validation === "zod") deps.push("zod@latest");
61
- if (context.validation === "yup") deps.push("yup@latest");
62
- if (context.tailwind && context.frontend === "react") deps.push("tailwindcss@latest", "@tailwindcss/vite@latest");
63
-
64
- await execa("npm", ["install", ...deps], { cwd: context.frontendDir, stdio: "ignore" });
65
- spinner.succeed("Installing frontend dependencies");
66
- } catch (error) {
67
- spinner.fail("Installing frontend dependencies");
68
- throw error;
69
- }
70
47
  }
71
48
 
72
49
  async function updateFrontendPackage(context) {
@@ -85,6 +62,7 @@ async function updateFrontendPackage(context) {
85
62
  if (context.frontend === "react" && context.router) deps.push("react-router-dom");
86
63
  if (context.state === "zustand") deps.push("zustand");
87
64
  if (context.state === "redux-toolkit") deps.push("@reduxjs/toolkit", "react-redux");
65
+ if (context.socketio) deps.push("socket.io-client");
88
66
  if (context.form && context.form !== "none") deps.push("react-hook-form", "@hookform/resolvers");
89
67
  if (context.validation === "zod") deps.push("zod");
90
68
  if (context.validation === "yup") deps.push("yup");
@@ -95,8 +73,21 @@ async function updateFrontendPackage(context) {
95
73
  pkg.devDependencies["@tailwindcss/vite"] = "latest";
96
74
  }
97
75
 
76
+ const backendName = path.basename(context.backendDir);
77
+ pkg.devDependencies = pkg.devDependencies || {};
78
+ pkg.devDependencies.concurrently = "latest";
79
+
98
80
  if (context.frontend === "react") {
99
- pkg.scripts = { ...(pkg.scripts || {}), build: "vite build" };
81
+ pkg.scripts = {
82
+ ...(pkg.scripts || {}),
83
+ build: "vite build",
84
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "vite"`,
85
+ };
86
+ } else {
87
+ pkg.scripts = {
88
+ ...(pkg.scripts || {}),
89
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "next dev"`,
90
+ };
100
91
  }
101
92
 
102
93
  await fs.writeJson(packageFile, pkg, { spaces: 2 });
@@ -123,12 +114,20 @@ async function writeReactOverlay(context) {
123
114
  await fs.ensureDir(path.join(src, "components", "ui"));
124
115
  await fs.ensureDir(path.join(src, "features", "auth"));
125
116
  await fs.ensureDir(path.join(src, "features", "todos"));
117
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
126
118
  await fs.ensureDir(path.join(src, "lib"));
127
119
  await fs.ensureDir(path.join(src, "pages"));
128
120
  await fs.ensureDir(path.join(src, "routes"));
129
- if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
121
+ if (context.state === "redux-toolkit") {
122
+ await fs.ensureDir(path.join(src, "redux", "slices"));
123
+ } else if (context.state !== "none") {
124
+ await fs.ensureDir(path.join(src, "store"));
125
+ }
130
126
 
131
- await writeTemplate(path.join(context.frontendDir, ".env"), "VITE_API_URL=http://localhost:5000/api\n");
127
+ await writeTemplate(
128
+ path.join(context.frontendDir, ".env"),
129
+ `VITE_API_URL=http://localhost:5000/api\n${context.socketio ? "VITE_SOCKET_URL=http://localhost:5000\n" : ""}`,
130
+ );
132
131
  await relaxTypescriptConfig(context, "react");
133
132
  if (context.tailwind) {
134
133
  await writeTemplate(path.join(context.frontendDir, "vite.config." + (context.language === "typescript" ? "ts" : "js")), `import { defineConfig } from "vite";
@@ -145,8 +144,7 @@ export default defineConfig({
145
144
  }
146
145
 
147
146
  const files = reactTemplates(context, x);
148
- if (context.state === "none") delete files[`store/auth.${x}`];
149
- if (context.validation === "none") delete files[`lib/validation.${context.language === "typescript" ? "ts" : "js"}`];
147
+ pruneUnusedFrontendFiles(files, context);
150
148
 
151
149
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
152
150
  if (context.language === "typescript") {
@@ -164,19 +162,58 @@ async function writeNextOverlay(context) {
164
162
  await fs.ensureDir(path.join(src, "components", "ui"));
165
163
  await fs.ensureDir(path.join(src, "features", "auth"));
166
164
  await fs.ensureDir(path.join(src, "features", "todos"));
165
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
167
166
  await fs.ensureDir(path.join(src, "lib"));
168
- if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
167
+ if (context.state === "redux-toolkit") {
168
+ await fs.ensureDir(path.join(src, "redux", "slices"));
169
+ } else if (context.state !== "none") {
170
+ await fs.ensureDir(path.join(src, "store"));
171
+ }
169
172
 
170
173
  await removeNextGeneratedDuplicates(context, x);
171
- await writeTemplate(path.join(context.frontendDir, ".env.local"), "NEXT_PUBLIC_API_URL=http://localhost:5000/api\n");
174
+ await writeTemplate(
175
+ path.join(context.frontendDir, ".env.local"),
176
+ `NEXT_PUBLIC_API_URL=http://localhost:5000/api\n${context.socketio ? "NEXT_PUBLIC_SOCKET_URL=http://localhost:5000\n" : ""}`,
177
+ );
172
178
  await relaxTypescriptConfig(context, "next");
173
179
  const files = nextTemplates(context, x);
174
- if (context.state === "none") delete files[`store/auth.${x}`];
175
- if (context.validation === "none") delete files[`lib/validation.${context.language === "typescript" ? "ts" : "js"}`];
180
+ pruneUnusedFrontendFiles(files, context);
176
181
 
177
182
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
178
183
  }
179
184
 
185
+ function pruneUnusedFrontendFiles(files, context) {
186
+ const x = jsxExt(context);
187
+ const isTs = context.language === "typescript";
188
+
189
+ if (context.state === "none") {
190
+ delete files[`store/auth.${x}`];
191
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
192
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
193
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
194
+ } else if (context.state === "redux-toolkit") {
195
+ delete files[`store/auth.${x}`];
196
+ } else {
197
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
198
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
199
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
200
+ }
201
+
202
+ if (!isTs) delete files["lib/types.js"];
203
+ if (context.validation === "none") {
204
+ delete files[`lib/validation.${isTs ? "ts" : "js"}`];
205
+ }
206
+
207
+ if (!context.socketio) {
208
+ delete files[`lib/socket.${isTs ? "ts" : "js"}`];
209
+ delete files[`features/chat/chatService.${isTs ? "ts" : "js"}`];
210
+ delete files[`pages/Chat.${x}`];
211
+ for (const key of Object.keys(files)) {
212
+ if (key.startsWith("app/chat/")) delete files[key];
213
+ }
214
+ }
215
+ }
216
+
180
217
  async function removeNextGeneratedDuplicates(context, x) {
181
218
  const appDir = path.join(context.frontendDir, "src", "app");
182
219
  const generated = ["js", "jsx", "ts", "tsx"].filter((candidate) => candidate !== x);
@@ -249,7 +286,7 @@ function commonTemplates(context, x) {
249
286
  const typeBlock = isTs ? `export type User = { id: string; name: string; email: string };
250
287
  export type Todo = { _id: string; title: string; description?: string; imageUrl?: string; status: "todo" | "in-progress" | "done" };
251
288
  ` : "";
252
- const schema = context.validation === "none" || context.validation === "joi"
289
+ const schema = context.validation === "none"
253
290
  ? `export const authSchema = null;
254
291
  export const todoSchema = null;
255
292
  `
@@ -301,7 +338,43 @@ export const useAuthStore = create((set) => ({
301
338
  }));
302
339
  `
303
340
  : context.state === "redux-toolkit"
304
- ? `import { configureStore, createSlice } from "@reduxjs/toolkit";
341
+ ? ""
342
+ : `import { createContext, useContext, useState } from "react";
343
+
344
+ const AuthContext = createContext(null);
345
+
346
+ export function AuthProvider({ children }) {
347
+ const [session, setSessionState] = useState({ user: null, token: typeof window !== "undefined" ? localStorage.getItem("stackflow_token") : null });
348
+ const setSession = ({ user, token }) => {
349
+ if (token && typeof window !== "undefined") localStorage.setItem("stackflow_token", token);
350
+ setSessionState({ user, token });
351
+ };
352
+ const logout = () => {
353
+ if (typeof window !== "undefined") localStorage.removeItem("stackflow_token");
354
+ setSessionState({ user: null, token: null });
355
+ };
356
+ return <AuthContext.Provider value={{ ...session, setSession, logout }}>{children}</AuthContext.Provider>;
357
+ }
358
+
359
+ export const useAuthContext = () => useContext(AuthContext);
360
+ `;
361
+
362
+ const reduxExt = isTs ? "ts" : "js";
363
+ const reduxFiles =
364
+ context.state === "redux-toolkit"
365
+ ? {
366
+ [`redux/store.${reduxExt}`]: `import { configureStore } from "@reduxjs/toolkit";
367
+ import authReducer from "./slices/authSlice";
368
+ import todoReducer from "./slices/todoSlices";
369
+
370
+ export const store = configureStore({
371
+ reducer: {
372
+ auth: authReducer,
373
+ todos: todoReducer
374
+ }
375
+ });
376
+ `,
377
+ [`redux/slices/authSlice.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
305
378
 
306
379
  const authSlice = createSlice({
307
380
  name: "auth",
@@ -321,35 +394,44 @@ const authSlice = createSlice({
321
394
  });
322
395
 
323
396
  export const { setSession, logout } = authSlice.actions;
324
- export const store = configureStore({ reducer: { auth: authSlice.reducer } });
325
- `
326
- : `import { createContext, useContext, useState } from "react";
327
-
328
- const AuthContext = createContext(null);
397
+ export default authSlice.reducer;
398
+ `,
399
+ [`redux/slices/todoSlices.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
329
400
 
330
- export function AuthProvider({ children }) {
331
- const [session, setSessionState] = useState({ user: null, token: typeof window !== "undefined" ? localStorage.getItem("stackflow_token") : null });
332
- const setSession = ({ user, token }) => {
333
- if (token && typeof window !== "undefined") localStorage.setItem("stackflow_token", token);
334
- setSessionState({ user, token });
335
- };
336
- const logout = () => {
337
- if (typeof window !== "undefined") localStorage.removeItem("stackflow_token");
338
- setSessionState({ user: null, token: null });
339
- };
340
- return <AuthContext.Provider value={{ ...session, setSession, logout }}>{children}</AuthContext.Provider>;
341
- }
401
+ const todoSlices = createSlice({
402
+ name: "todos",
403
+ initialState: { items: [] },
404
+ reducers: {
405
+ setTodos: (state, action) => {
406
+ state.items = action.payload;
407
+ },
408
+ addTodo: (state, action) => {
409
+ state.items.unshift(action.payload);
410
+ },
411
+ updateTodo: (state, action) => {
412
+ const index = state.items.findIndex((todo) => todo._id === action.payload._id);
413
+ if (index !== -1) state.items[index] = action.payload;
414
+ },
415
+ removeTodo: (state, action) => {
416
+ state.items = state.items.filter((todo) => todo._id !== action.payload);
417
+ }
418
+ }
419
+ });
342
420
 
343
- export const useAuthContext = () => useContext(AuthContext);
344
- `;
421
+ export const { setTodos, addTodo, updateTodo, removeTodo } = todoSlices.actions;
422
+ export default todoSlices.reducer;
423
+ `,
424
+ }
425
+ : {};
345
426
 
346
427
  return {
347
- [`lib/types.${isTs ? "ts" : "js"}`]: typeBlock || "export {};\n",
428
+ ...(isTs ? { "lib/types.ts": typeBlock } : {}),
429
+ ...reduxFiles,
430
+ ...chatFrontendFiles(context, x),
348
431
  [`lib/api.${isTs ? "ts" : "js"}`]: `import axios from "axios";
349
432
 
350
433
  export const api = axios.create({
351
- baseURL: ${context.frontend === "next" ? "process.env.NEXT_PUBLIC_API_URL" : "import.meta.env.VITE_API_URL"} || "http://localhost:5000/api",
352
- withCredentials: true
434
+ baseURL: ${context.frontend === "next" ? "process.env.NEXT_PUBLIC_API_URL" : "import.meta.env.VITE_API_URL"} || "http://localhost:5000/api"
353
435
  });
354
436
 
355
437
  api.interceptors.request.use((config) => {
@@ -358,14 +440,16 @@ api.interceptors.request.use((config) => {
358
440
  return config;
359
441
  });
360
442
  `,
361
- [`lib/validation.${isTs ? "ts" : "js"}`]: schema,
362
- [`store/auth.${x}`]: authStore,
443
+ ...(context.validation !== "none" ? { [`lib/validation.${isTs ? "ts" : "js"}`]: schema } : {}),
444
+ ...(context.state !== "none" && context.state !== "redux-toolkit"
445
+ ? { [`store/auth.${x}`]: authStore }
446
+ : {}),
363
447
  [`features/auth/authService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
364
448
 
365
449
  export const authService = {
366
- register: (payload) => api.post("/auth/register", payload).then((res) => res.data),
367
- login: (payload) => api.post("/auth/login", payload).then((res) => res.data),
368
- me: () => api.get("/auth/me").then((res) => res.data),
450
+ register: (payload) => api.post("/auth/register", payload).then((res) => res.data.data),
451
+ login: (payload) => api.post("/auth/login", payload).then((res) => res.data.data),
452
+ me: () => api.get("/auth/me").then((res) => res.data.data),
369
453
  logout: () => api.post("/auth/logout").then((res) => res.data)
370
454
  };
371
455
  `,
@@ -421,7 +505,7 @@ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, B
421
505
  [`main.${x}`]: `import React from "react";
422
506
  import ReactDOM from "react-dom/client";
423
507
  ${context.router ? "import { BrowserRouter } from \"react-router-dom\";" : ""}
424
- ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./store/auth\";" : ""}
508
+ ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./redux/store\";" : ""}
425
509
  ${context.state === "context-api" ? "import { AuthProvider } from \"./store/auth\";" : ""}
426
510
  import { Toaster } from "sonner";
427
511
  import App from "./App";
@@ -445,14 +529,17 @@ import { Dashboard } from "./pages/Dashboard";
445
529
  import { Login } from "./pages/Login";
446
530
  import { Register } from "./pages/Register";
447
531
  import { ProtectedRoute } from "./routes/ProtectedRoute";
532
+ ${context.socketio ? 'import { Chat } from "./pages/Chat";' : ""}
448
533
 
449
534
  export default function App() {
450
535
  return (
451
536
  <Routes>
452
- <Route path="/" element={<Navigate to="/dashboard" replace />} />
537
+ <Route path="/" element={<Navigate to="${context.socketio ? "/chat" : "/dashboard"}" replace />} />
453
538
  <Route path="/login" element={<Login />} />
454
539
  <Route path="/register" element={<Register />} />
455
540
  <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
541
+ ${context.socketio ? `<Route path="/chat" element={<ProtectedRoute><Chat /></ProtectedRoute>} />
542
+ <Route path="/chat/:conversationId" element={<ProtectedRoute><Chat /></ProtectedRoute>} />` : ""}
456
543
  </Routes>
457
544
  );
458
545
  }
@@ -478,6 +565,7 @@ export function ProtectedRoute({ children }) {
478
565
  function nextTemplates(context, x) {
479
566
  return {
480
567
  ...commonTemplates(context, x),
568
+ ...chatNextPages(context, x),
481
569
  "app/globals.css": `@import "tailwindcss";
482
570
  @custom-variant dark (&:where(.dark, .dark *));
483
571
 
@@ -502,7 +590,7 @@ export default function RootLayout({ children }) {
502
590
  [`app/page.${x}`]: `import { redirect } from "next/navigation";
503
591
 
504
592
  export default function Home() {
505
- redirect("/dashboard");
593
+ redirect("${context.socketio ? "/chat" : "/dashboard"}");
506
594
  }
507
595
  `,
508
596
  [`app/login/page.${x}`]: `"use client";
@@ -524,7 +612,9 @@ function authPage(mode, context, next = false) {
524
612
  const isRegister = mode === "register";
525
613
  const navImport = next ? "import { useRouter } from \"next/navigation\";" : "import { Link, useNavigate } from \"react-router-dom\";";
526
614
  const navHook = next ? "const router = useRouter();" : "const navigate = useNavigate();";
527
- const redirect = next ? "router.push(\"/dashboard\");" : "navigate(\"/dashboard\");";
615
+ const redirect = next
616
+ ? `router.push("${context.socketio ? "/chat" : "/dashboard"}");`
617
+ : `navigate("${context.socketio ? "/chat" : "/dashboard"}");`;
528
618
  const link = next
529
619
  ? `<button className="text-sm text-cyan-700" onClick={() => router.push("${isRegister ? "/login" : "/register"}")}>${isRegister ? "Sign in" : "Create account"}</button>`
530
620
  : `<Link className="text-sm text-cyan-700" to="${isRegister ? "/login" : "/register"}">${isRegister ? "Sign in" : "Create account"}</Link>`;
@@ -536,7 +626,7 @@ import { Input } from "${next ? "../../components/ui/input" : "../components/ui/
536
626
  import { authService } from "${next ? "../../features/auth/authService" : "../features/auth/authService"}";
537
627
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
538
628
  ${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
539
- ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { setSession as setReduxSession } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
629
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { setSession as setReduxSession } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
540
630
 
541
631
  export function ${isRegister ? "Register" : "Login"}() {
542
632
  ${navHook}
@@ -597,7 +687,7 @@ import { Input } from "${next ? "../../components/ui/input" : "../components/ui/
597
687
  import { todoService } from "${next ? "../../features/todos/todoService" : "../features/todos/todoService"}";
598
688
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
599
689
  ${context.state === "context-api" ? `import { useAuthContext } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
600
- ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { logout as reduxLogout } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
690
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { logout as reduxLogout } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
601
691
 
602
692
  ${componentName} {
603
693
  const ${next ? "router" : "navigate"} = ${next ? "useRouter()" : "useNavigate()"};
@@ -1,62 +1,7 @@
1
1
  import path from "node:path";
2
2
  import fs from "fs-extra";
3
- import { writeTemplate } from "../utils/template.js";
4
3
 
5
- export async function createRootFiles(context) {
6
- const frontendName = path.basename(context.frontendDir);
7
- const backendName = path.basename(context.backendDir);
8
-
9
- await writeTemplate(
10
- path.join(context.projectDir, "package.json"),
11
- `{
12
- "name": "{{projectName}}",
13
- "version": "1.0.0",
14
- "private": true,
15
- "scripts": {
16
- "dev": "concurrently \\"npm run dev --workspace {{backendName}}\\" \\"npm run dev --workspace {{frontendName}}\\"",
17
- "dev:frontend": "npm run dev --workspace {{frontendName}}",
18
- "dev:backend": "npm run dev --workspace {{backendName}}"
19
- },
20
- "workspaces": [
21
- "{{frontendName}}",
22
- "{{backendName}}"
23
- ],
24
- "devDependencies": {
25
- "concurrently": "latest"
26
- }
27
- }
28
- `,
29
- { ...context, frontendName, backendName }
30
- );
31
-
32
- await writeTemplate(
33
- path.join(context.projectDir, "README.md"),
34
- `# {{projectName}}
35
-
36
- Generated with \`create-stackflow\`.
37
-
38
- ## Development
39
-
40
- \`\`\`bash
41
- npm run dev
42
- \`\`\`
43
-
44
- Frontend runs on Vite/Next.js. Backend runs on Express at \`http://localhost:5000\`.
45
-
46
- ## Environment
47
-
48
- MongoDB local connection:
49
-
50
- \`\`\`
51
- mongodb://127.0.0.1:27017/{{databaseName}}
52
- \`\`\`
53
- `,
54
- context
55
- );
56
-
57
- await fs.outputFile(
58
- path.join(context.projectDir, ".gitignore"),
59
- `node_modules
4
+ const GITIGNORE = `node_modules
60
5
  .env
61
6
  .env.*
62
7
  dist
@@ -65,6 +10,9 @@ build
65
10
  coverage
66
11
  uploads/*
67
12
  !uploads/.gitkeep
68
- `
69
- );
13
+ `;
14
+
15
+ export async function createRootFiles(context) {
16
+ await fs.outputFile(path.join(context.frontendDir, ".gitignore"), GITIGNORE);
17
+ await fs.outputFile(path.join(context.backendDir, ".gitignore"), GITIGNORE);
70
18
  }
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import fs from "fs-extra";
3
+ import { execa } from "execa";
4
+
5
+ /**
6
+ * Hoist dependencies into project-root/node_modules via a temporary workspace
7
+ * package.json, then remove root package.json and lockfile so only
8
+ * frontend/, backend/, and node_modules/ remain at the project root.
9
+ */
10
+ export async function installHoistedWorkspace(context) {
11
+ const frontendName = path.basename(context.frontendDir);
12
+ const backendName = path.basename(context.backendDir);
13
+ const rootPkgPath = path.join(context.projectDir, "package.json");
14
+ const rootLockPath = path.join(context.projectDir, "package-lock.json");
15
+
16
+ await fs.writeJson(
17
+ rootPkgPath,
18
+ {
19
+ name: context.projectName,
20
+ private: true,
21
+ workspaces: [frontendName, backendName],
22
+ },
23
+ { spaces: 2 },
24
+ );
25
+
26
+ await execa("npm", ["install"], { cwd: context.projectDir, stdio: "ignore" });
27
+
28
+ await fs.remove(rootPkgPath);
29
+ if (await fs.pathExists(rootLockPath)) {
30
+ await fs.remove(rootLockPath);
31
+ }
32
+
33
+ await removeNestedNodeModules(context.frontendDir);
34
+ await removeNestedNodeModules(context.backendDir);
35
+ }
36
+
37
+ async function removeNestedNodeModules(dir) {
38
+ const nested = path.join(dir, "node_modules");
39
+ if (await fs.pathExists(nested)) {
40
+ await fs.remove(nested);
41
+ }
42
+ }
@@ -143,7 +143,6 @@ export async function askQuestions(projectName) {
143
143
  choices: [
144
144
  { name: "Zod", value: "zod" },
145
145
  { name: "Yup", value: "yup" },
146
- { name: "Joi", value: "joi" },
147
146
  { name: "None", value: "none" },
148
147
  ],
149
148
  default: "zod",
@@ -247,6 +246,16 @@ export async function askQuestions(projectName) {
247
246
  ],
248
247
  default: "javascript",
249
248
  },
249
+ {
250
+ type: "list",
251
+ name: "backendModule",
252
+ message: "Backend module system?",
253
+ choices: [
254
+ { name: "ES Modules (import/export)", value: "esm" },
255
+ { name: "CommonJS (require/module.exports)", value: "cjs" },
256
+ ],
257
+ default: "esm",
258
+ },
250
259
  {
251
260
  type: "list",
252
261
  name: "database",
@@ -311,6 +320,19 @@ export async function askQuestions(projectName) {
311
320
  { name: "hpp", value: "hpp" },
312
321
  ],
313
322
  },
323
+ {
324
+ type: "confirm",
325
+ name: "swagger",
326
+ message: "Add Swagger API documentation?",
327
+ default: false,
328
+ },
329
+ {
330
+ type: "confirm",
331
+ name: "socketio",
332
+ message:
333
+ "Add real-time chat (Socket.IO)? Includes WhatsApp-style chat requests + 1:1 messaging",
334
+ default: false,
335
+ },
314
336
  {
315
337
  type: "confirm",
316
338
  name: "runProject",
@@ -327,6 +349,7 @@ export async function askQuestions(projectName) {
327
349
  tailwind: frontendAnswers.styling === "tailwind",
328
350
  shadcn: frontendAnswers.uiLibrary === "shadcn",
329
351
  axios: frontendAnswers.dataFetching === "axios",
352
+ form: frontendAnswers.formLibrary,
330
353
  reactHookForm: frontendAnswers.formLibrary === "react-hook-form",
331
354
 
332
355
  helmet: backendAnswers.securityPackages.includes("helmet"),
@@ -336,6 +359,8 @@ export async function askQuestions(projectName) {
336
359
 
337
360
  multer: backendAnswers.fileUpload === "Multer",
338
361
  cloudinary: backendAnswers.fileUpload === "Cloudinary",
362
+ swagger: backendAnswers.swagger,
363
+ socketio: backendAnswers.socketio,
339
364
  runProject: backendAnswers.runProject,
340
365
  };
341
366
  }