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.
- package/README.md +4 -6
- package/cli.js +1 -1
- package/package.json +1 -1
- package/src/commands/create.js +9 -8
- package/src/generators/backend.js +156 -129
- package/src/generators/chat.js +661 -0
- package/src/generators/frontend.js +158 -68
- package/src/generators/root.js +6 -58
- package/src/utils/install.js +42 -0
- package/src/utils/prompts.js +26 -1
|
@@ -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 = {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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"
|
|
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
|
-
?
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
344
|
-
|
|
421
|
+
export const { setTodos, addTodo, updateTodo, removeTodo } = todoSlices.actions;
|
|
422
|
+
export default todoSlices.reducer;
|
|
423
|
+
`,
|
|
424
|
+
}
|
|
425
|
+
: {};
|
|
345
426
|
|
|
346
427
|
return {
|
|
347
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ? "../../
|
|
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 ? "../../
|
|
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()"};
|
package/src/generators/root.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/src/utils/prompts.js
CHANGED
|
@@ -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
|
}
|