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.
- package/README.md +4 -6
- package/cli.js +1 -1
- package/package.json +1 -1
- package/src/commands/create.js +10 -9
- package/src/generators/backend.js +156 -129
- package/src/generators/chat.js +661 -0
- package/src/generators/frontend.js +184 -73
- 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);
|
|
@@ -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: "
|
|
30
|
+
await execa("npx", args, { cwd: context.projectDir, stdio: "inherit" });
|
|
30
31
|
} else {
|
|
31
32
|
const template = context.language === "typescript" ? "react-ts" : "react";
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 = {
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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"
|
|
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
|
-
?
|
|
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
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
|
344
|
-
|
|
442
|
+
export const { setTodos, addTodo, updateTodo, removeTodo } = todoSlices.actions;
|
|
443
|
+
export default todoSlices.reducer;
|
|
444
|
+
`,
|
|
445
|
+
}
|
|
446
|
+
: {};
|
|
345
447
|
|
|
346
448
|
return {
|
|
347
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ? "../../
|
|
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 ? "../../
|
|
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()"};
|
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
|
}
|