create-stackflow 1.0.5 → 1.0.8

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);
@@ -26,14 +27,25 @@ async function scaffoldOfficialFrontend(context) {
26
27
  "--import-alias",
27
28
  "@/*"
28
29
  ];
29
- await execa("npx", args, { cwd: context.projectDir, stdio: "ignore" });
30
+ await execa("npx", args, { cwd: context.projectDir, stdio: "inherit" });
30
31
  } else {
31
32
  const template = context.language === "typescript" ? "react-ts" : "react";
32
- await execa("npm", ["create", "vite@latest", path.basename(context.frontendDir), "--", "--template", template], {
33
- cwd: context.projectDir,
34
- stdio: "ignore"
35
- });
33
+ const frontendName = path.basename(context.frontendDir);
34
+ if (await fs.pathExists(context.frontendDir)) {
35
+ const existing = await fs.readdir(context.frontendDir);
36
+ if (existing.length > 0) {
37
+ throw new Error(
38
+ `Frontend folder "${frontendName}" already exists and is not empty. Remove it or choose another frontend folder name.`,
39
+ );
40
+ }
41
+ }
42
+ await execa(
43
+ "npm",
44
+ ["create", "vite@latest", frontendName, "--", "--template", template, "--no-interactive"],
45
+ { cwd: context.projectDir, stdio: "inherit" },
46
+ );
36
47
  }
48
+ await assertFrontendScaffolded(context);
37
49
  spinner.succeed("Creating frontend with official generator");
38
50
  } catch (error) {
39
51
  spinner.fail("Creating frontend with official generator");
@@ -41,32 +53,18 @@ async function scaffoldOfficialFrontend(context) {
41
53
  }
42
54
  }
43
55
 
56
+ async function assertFrontendScaffolded(context) {
57
+ const packageFile = path.join(context.frontendDir, "package.json");
58
+ if (!(await fs.pathExists(packageFile))) {
59
+ throw new Error(
60
+ `Frontend scaffold did not create package.json at ${packageFile}. ` +
61
+ "The official generator may have been cancelled or failed. Try again in an empty project folder.",
62
+ );
63
+ }
64
+ }
65
+
44
66
  async function installFrontendDependencies(context) {
45
67
  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
68
  }
71
69
 
72
70
  async function updateFrontendPackage(context) {
@@ -85,6 +83,7 @@ async function updateFrontendPackage(context) {
85
83
  if (context.frontend === "react" && context.router) deps.push("react-router-dom");
86
84
  if (context.state === "zustand") deps.push("zustand");
87
85
  if (context.state === "redux-toolkit") deps.push("@reduxjs/toolkit", "react-redux");
86
+ if (context.socketio) deps.push("socket.io-client");
88
87
  if (context.form && context.form !== "none") deps.push("react-hook-form", "@hookform/resolvers");
89
88
  if (context.validation === "zod") deps.push("zod");
90
89
  if (context.validation === "yup") deps.push("yup");
@@ -95,8 +94,21 @@ async function updateFrontendPackage(context) {
95
94
  pkg.devDependencies["@tailwindcss/vite"] = "latest";
96
95
  }
97
96
 
97
+ const backendName = path.basename(context.backendDir);
98
+ pkg.devDependencies = pkg.devDependencies || {};
99
+ pkg.devDependencies.concurrently = "^9.2.1";
100
+
98
101
  if (context.frontend === "react") {
99
- pkg.scripts = { ...(pkg.scripts || {}), build: "vite build" };
102
+ pkg.scripts = {
103
+ ...(pkg.scripts || {}),
104
+ build: "vite build",
105
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "vite"`,
106
+ };
107
+ } else {
108
+ pkg.scripts = {
109
+ ...(pkg.scripts || {}),
110
+ dev: `concurrently -n backend,frontend -c blue,green "npm run dev --prefix ../${backendName}" "next dev"`,
111
+ };
100
112
  }
101
113
 
102
114
  await fs.writeJson(packageFile, pkg, { spaces: 2 });
@@ -123,12 +135,20 @@ async function writeReactOverlay(context) {
123
135
  await fs.ensureDir(path.join(src, "components", "ui"));
124
136
  await fs.ensureDir(path.join(src, "features", "auth"));
125
137
  await fs.ensureDir(path.join(src, "features", "todos"));
138
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
126
139
  await fs.ensureDir(path.join(src, "lib"));
127
140
  await fs.ensureDir(path.join(src, "pages"));
128
141
  await fs.ensureDir(path.join(src, "routes"));
129
- if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
142
+ if (context.state === "redux-toolkit") {
143
+ await fs.ensureDir(path.join(src, "redux", "slices"));
144
+ } else if (context.state !== "none") {
145
+ await fs.ensureDir(path.join(src, "store"));
146
+ }
130
147
 
131
- await writeTemplate(path.join(context.frontendDir, ".env"), "VITE_API_URL=http://localhost:5000/api\n");
148
+ await writeTemplate(
149
+ path.join(context.frontendDir, ".env"),
150
+ `VITE_API_URL=http://localhost:5000/api\n${context.socketio ? "VITE_SOCKET_URL=http://localhost:5000\n" : ""}`,
151
+ );
132
152
  await relaxTypescriptConfig(context, "react");
133
153
  if (context.tailwind) {
134
154
  await writeTemplate(path.join(context.frontendDir, "vite.config." + (context.language === "typescript" ? "ts" : "js")), `import { defineConfig } from "vite";
@@ -145,8 +165,7 @@ export default defineConfig({
145
165
  }
146
166
 
147
167
  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"}`];
168
+ pruneUnusedFrontendFiles(files, context);
150
169
 
151
170
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
152
171
  if (context.language === "typescript") {
@@ -164,19 +183,58 @@ async function writeNextOverlay(context) {
164
183
  await fs.ensureDir(path.join(src, "components", "ui"));
165
184
  await fs.ensureDir(path.join(src, "features", "auth"));
166
185
  await fs.ensureDir(path.join(src, "features", "todos"));
186
+ if (context.socketio) await fs.ensureDir(path.join(src, "features", "chat"));
167
187
  await fs.ensureDir(path.join(src, "lib"));
168
- if (context.state !== "none") await fs.ensureDir(path.join(src, "store"));
188
+ if (context.state === "redux-toolkit") {
189
+ await fs.ensureDir(path.join(src, "redux", "slices"));
190
+ } else if (context.state !== "none") {
191
+ await fs.ensureDir(path.join(src, "store"));
192
+ }
169
193
 
170
194
  await removeNextGeneratedDuplicates(context, x);
171
- await writeTemplate(path.join(context.frontendDir, ".env.local"), "NEXT_PUBLIC_API_URL=http://localhost:5000/api\n");
195
+ await writeTemplate(
196
+ path.join(context.frontendDir, ".env.local"),
197
+ `NEXT_PUBLIC_API_URL=http://localhost:5000/api\n${context.socketio ? "NEXT_PUBLIC_SOCKET_URL=http://localhost:5000\n" : ""}`,
198
+ );
172
199
  await relaxTypescriptConfig(context, "next");
173
200
  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"}`];
201
+ pruneUnusedFrontendFiles(files, context);
176
202
 
177
203
  await Promise.all(Object.entries(files).map(([file, content]) => writeTemplate(path.join(src, file), content, context)));
178
204
  }
179
205
 
206
+ function pruneUnusedFrontendFiles(files, context) {
207
+ const x = jsxExt(context);
208
+ const isTs = context.language === "typescript";
209
+
210
+ if (context.state === "none") {
211
+ delete files[`store/auth.${x}`];
212
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
213
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
214
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
215
+ } else if (context.state === "redux-toolkit") {
216
+ delete files[`store/auth.${x}`];
217
+ } else {
218
+ delete files[`redux/store.${isTs ? "ts" : "js"}`];
219
+ delete files[`redux/slices/authSlice.${isTs ? "ts" : "js"}`];
220
+ delete files[`redux/slices/todoSlices.${isTs ? "ts" : "js"}`];
221
+ }
222
+
223
+ if (!isTs) delete files["lib/types.js"];
224
+ if (context.validation === "none") {
225
+ delete files[`lib/validation.${isTs ? "ts" : "js"}`];
226
+ }
227
+
228
+ if (!context.socketio) {
229
+ delete files[`lib/socket.${isTs ? "ts" : "js"}`];
230
+ delete files[`features/chat/chatService.${isTs ? "ts" : "js"}`];
231
+ delete files[`pages/Chat.${x}`];
232
+ for (const key of Object.keys(files)) {
233
+ if (key.startsWith("app/chat/")) delete files[key];
234
+ }
235
+ }
236
+ }
237
+
180
238
  async function removeNextGeneratedDuplicates(context, x) {
181
239
  const appDir = path.join(context.frontendDir, "src", "app");
182
240
  const generated = ["js", "jsx", "ts", "tsx"].filter((candidate) => candidate !== x);
@@ -249,7 +307,7 @@ function commonTemplates(context, x) {
249
307
  const typeBlock = isTs ? `export type User = { id: string; name: string; email: string };
250
308
  export type Todo = { _id: string; title: string; description?: string; imageUrl?: string; status: "todo" | "in-progress" | "done" };
251
309
  ` : "";
252
- const schema = context.validation === "none" || context.validation === "joi"
310
+ const schema = context.validation === "none"
253
311
  ? `export const authSchema = null;
254
312
  export const todoSchema = null;
255
313
  `
@@ -301,7 +359,43 @@ export const useAuthStore = create((set) => ({
301
359
  }));
302
360
  `
303
361
  : context.state === "redux-toolkit"
304
- ? `import { configureStore, createSlice } from "@reduxjs/toolkit";
362
+ ? ""
363
+ : `import { createContext, useContext, useState } from "react";
364
+
365
+ const AuthContext = createContext(null);
366
+
367
+ export function AuthProvider({ children }) {
368
+ const [session, setSessionState] = useState({ user: null, token: typeof window !== "undefined" ? localStorage.getItem("stackflow_token") : null });
369
+ const setSession = ({ user, token }) => {
370
+ if (token && typeof window !== "undefined") localStorage.setItem("stackflow_token", token);
371
+ setSessionState({ user, token });
372
+ };
373
+ const logout = () => {
374
+ if (typeof window !== "undefined") localStorage.removeItem("stackflow_token");
375
+ setSessionState({ user: null, token: null });
376
+ };
377
+ return <AuthContext.Provider value={{ ...session, setSession, logout }}>{children}</AuthContext.Provider>;
378
+ }
379
+
380
+ export const useAuthContext = () => useContext(AuthContext);
381
+ `;
382
+
383
+ const reduxExt = isTs ? "ts" : "js";
384
+ const reduxFiles =
385
+ context.state === "redux-toolkit"
386
+ ? {
387
+ [`redux/store.${reduxExt}`]: `import { configureStore } from "@reduxjs/toolkit";
388
+ import authReducer from "./slices/authSlice";
389
+ import todoReducer from "./slices/todoSlices";
390
+
391
+ export const store = configureStore({
392
+ reducer: {
393
+ auth: authReducer,
394
+ todos: todoReducer
395
+ }
396
+ });
397
+ `,
398
+ [`redux/slices/authSlice.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
305
399
 
306
400
  const authSlice = createSlice({
307
401
  name: "auth",
@@ -321,35 +415,44 @@ const authSlice = createSlice({
321
415
  });
322
416
 
323
417
  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);
418
+ export default authSlice.reducer;
419
+ `,
420
+ [`redux/slices/todoSlices.${reduxExt}`]: `import { createSlice } from "@reduxjs/toolkit";
329
421
 
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
- }
422
+ const todoSlices = createSlice({
423
+ name: "todos",
424
+ initialState: { items: [] },
425
+ reducers: {
426
+ setTodos: (state, action) => {
427
+ state.items = action.payload;
428
+ },
429
+ addTodo: (state, action) => {
430
+ state.items.unshift(action.payload);
431
+ },
432
+ updateTodo: (state, action) => {
433
+ const index = state.items.findIndex((todo) => todo._id === action.payload._id);
434
+ if (index !== -1) state.items[index] = action.payload;
435
+ },
436
+ removeTodo: (state, action) => {
437
+ state.items = state.items.filter((todo) => todo._id !== action.payload);
438
+ }
439
+ }
440
+ });
342
441
 
343
- export const useAuthContext = () => useContext(AuthContext);
344
- `;
442
+ export const { setTodos, addTodo, updateTodo, removeTodo } = todoSlices.actions;
443
+ export default todoSlices.reducer;
444
+ `,
445
+ }
446
+ : {};
345
447
 
346
448
  return {
347
- [`lib/types.${isTs ? "ts" : "js"}`]: typeBlock || "export {};\n",
449
+ ...(isTs ? { "lib/types.ts": typeBlock } : {}),
450
+ ...reduxFiles,
451
+ ...chatFrontendFiles(context, x),
348
452
  [`lib/api.${isTs ? "ts" : "js"}`]: `import axios from "axios";
349
453
 
350
454
  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
455
+ baseURL: ${context.frontend === "next" ? "process.env.NEXT_PUBLIC_API_URL" : "import.meta.env.VITE_API_URL"} || "http://localhost:5000/api"
353
456
  });
354
457
 
355
458
  api.interceptors.request.use((config) => {
@@ -358,14 +461,16 @@ api.interceptors.request.use((config) => {
358
461
  return config;
359
462
  });
360
463
  `,
361
- [`lib/validation.${isTs ? "ts" : "js"}`]: schema,
362
- [`store/auth.${x}`]: authStore,
464
+ ...(context.validation !== "none" ? { [`lib/validation.${isTs ? "ts" : "js"}`]: schema } : {}),
465
+ ...(context.state !== "none" && context.state !== "redux-toolkit"
466
+ ? { [`store/auth.${x}`]: authStore }
467
+ : {}),
363
468
  [`features/auth/authService.${isTs ? "ts" : "js"}`]: `import { api } from "../../lib/api";
364
469
 
365
470
  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),
471
+ register: (payload) => api.post("/auth/register", payload).then((res) => res.data.data),
472
+ login: (payload) => api.post("/auth/login", payload).then((res) => res.data.data),
473
+ me: () => api.get("/auth/me").then((res) => res.data.data),
369
474
  logout: () => api.post("/auth/logout").then((res) => res.data)
370
475
  };
371
476
  `,
@@ -421,7 +526,7 @@ body { margin: 0; font-family: Inter, ui-sans-serif, system-ui, -apple-system, B
421
526
  [`main.${x}`]: `import React from "react";
422
527
  import ReactDOM from "react-dom/client";
423
528
  ${context.router ? "import { BrowserRouter } from \"react-router-dom\";" : ""}
424
- ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./store/auth\";" : ""}
529
+ ${context.state === "redux-toolkit" ? "import { Provider } from \"react-redux\";\nimport { store } from \"./redux/store\";" : ""}
425
530
  ${context.state === "context-api" ? "import { AuthProvider } from \"./store/auth\";" : ""}
426
531
  import { Toaster } from "sonner";
427
532
  import App from "./App";
@@ -445,14 +550,17 @@ import { Dashboard } from "./pages/Dashboard";
445
550
  import { Login } from "./pages/Login";
446
551
  import { Register } from "./pages/Register";
447
552
  import { ProtectedRoute } from "./routes/ProtectedRoute";
553
+ ${context.socketio ? 'import { Chat } from "./pages/Chat";' : ""}
448
554
 
449
555
  export default function App() {
450
556
  return (
451
557
  <Routes>
452
- <Route path="/" element={<Navigate to="/dashboard" replace />} />
558
+ <Route path="/" element={<Navigate to="${context.socketio ? "/chat" : "/dashboard"}" replace />} />
453
559
  <Route path="/login" element={<Login />} />
454
560
  <Route path="/register" element={<Register />} />
455
561
  <Route path="/dashboard" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
562
+ ${context.socketio ? `<Route path="/chat" element={<ProtectedRoute><Chat /></ProtectedRoute>} />
563
+ <Route path="/chat/:conversationId" element={<ProtectedRoute><Chat /></ProtectedRoute>} />` : ""}
456
564
  </Routes>
457
565
  );
458
566
  }
@@ -478,6 +586,7 @@ export function ProtectedRoute({ children }) {
478
586
  function nextTemplates(context, x) {
479
587
  return {
480
588
  ...commonTemplates(context, x),
589
+ ...chatNextPages(context, x),
481
590
  "app/globals.css": `@import "tailwindcss";
482
591
  @custom-variant dark (&:where(.dark, .dark *));
483
592
 
@@ -502,7 +611,7 @@ export default function RootLayout({ children }) {
502
611
  [`app/page.${x}`]: `import { redirect } from "next/navigation";
503
612
 
504
613
  export default function Home() {
505
- redirect("/dashboard");
614
+ redirect("${context.socketio ? "/chat" : "/dashboard"}");
506
615
  }
507
616
  `,
508
617
  [`app/login/page.${x}`]: `"use client";
@@ -524,7 +633,9 @@ function authPage(mode, context, next = false) {
524
633
  const isRegister = mode === "register";
525
634
  const navImport = next ? "import { useRouter } from \"next/navigation\";" : "import { Link, useNavigate } from \"react-router-dom\";";
526
635
  const navHook = next ? "const router = useRouter();" : "const navigate = useNavigate();";
527
- const redirect = next ? "router.push(\"/dashboard\");" : "navigate(\"/dashboard\");";
636
+ const redirect = next
637
+ ? `router.push("${context.socketio ? "/chat" : "/dashboard"}");`
638
+ : `navigate("${context.socketio ? "/chat" : "/dashboard"}");`;
528
639
  const link = next
529
640
  ? `<button className="text-sm text-cyan-700" onClick={() => router.push("${isRegister ? "/login" : "/register"}")}>${isRegister ? "Sign in" : "Create account"}</button>`
530
641
  : `<Link className="text-sm text-cyan-700" to="${isRegister ? "/login" : "/register"}">${isRegister ? "Sign in" : "Create account"}</Link>`;
@@ -536,7 +647,7 @@ import { Input } from "${next ? "../../components/ui/input" : "../components/ui/
536
647
  import { authService } from "${next ? "../../features/auth/authService" : "../features/auth/authService"}";
537
648
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
538
649
  ${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"}";` : ""}
650
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { setSession as setReduxSession } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
540
651
 
541
652
  export function ${isRegister ? "Register" : "Login"}() {
542
653
  ${navHook}
@@ -597,7 +708,7 @@ import { Input } from "${next ? "../../components/ui/input" : "../components/ui/
597
708
  import { todoService } from "${next ? "../../features/todos/todoService" : "../features/todos/todoService"}";
598
709
  ${context.state === "zustand" ? `import { useAuthStore } from "${next ? "../../store/auth" : "../store/auth"}";` : ""}
599
710
  ${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"}";` : ""}
711
+ ${context.state === "redux-toolkit" ? `import { useDispatch } from "react-redux";\nimport { logout as reduxLogout } from "${next ? "../../redux/slices/authSlice" : "../redux/slices/authSlice"}";` : ""}
601
712
 
602
713
  ${componentName} {
603
714
  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
  }