tss-stack 1.2.0 → 1.2.1

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 CHANGED
@@ -283,44 +283,6 @@ tss-stack/
283
283
  │ └── utils.js ← shared helpers (toPascal, toRoute, toCamel)
284
284
  └── package.json
285
285
  ```
286
-
287
- ---
288
-
289
- ## Local Development
290
-
291
- Clone the repo and link it globally to test before publishing:
292
-
293
- ```bash
294
- git clone https://github.com/your-username/tss-stack.git
295
- cd tss-stack
296
- npm install
297
- npm link
298
- ```
299
-
300
- Now test it like a real user:
301
-
302
- ```bash
303
- tss-stack test-project
304
- ```
305
-
306
- When done testing:
307
-
308
- ```bash
309
- npm unlink -g tss-stack
310
- ```
311
-
312
- ---
313
-
314
- ## Publishing a New Version
315
-
316
- ```bash
317
- npm version patch # bug fix: 1.0.0 → 1.0.1
318
- npm version minor # new feature: 1.0.0 → 1.1.0
319
- npm version major # breaking: 1.0.0 → 2.0.0
320
-
321
- npm publish
322
- ```
323
-
324
286
  ---
325
287
 
326
288
  ## Roadmap
package/bin/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
4
  const ora = require("ora");
@@ -310,14 +310,26 @@ process.stdout.write("\x1Bc");
310
310
 
311
311
  async function showProjectTree(config) {
312
312
  const backendRouteFiles = [
313
- config.needsAuth ? "│ ├── auth.js" : null,
314
- ...config.tables.map((t) => `│ ├── ${t.name}.js`),
313
+ config.needsAuth ? "│ ├── auth.js" : null,
314
+ ...config.tables.map((t, i) => {
315
+ const isLast = i === config.tables.length - 1;
316
+ return `│ │ ${isLast && !config.tables.slice(i + 1).length ? "└──" : "├──"} ${t.name}.js`;
317
+ }),
315
318
  ].filter(Boolean);
316
319
 
320
+ const frontendContextFiles = config.needsAuth ? [
321
+ "│ │ ├── AuthContext.jsx",
322
+ ] : [];
323
+
324
+ const frontendComponentFiles = config.needsAuth ? [
325
+ "│ │ └── PrivateRoute.jsx",
326
+ ] : [];
327
+
317
328
  const frontendPageFiles = [
318
- config.needsAuth ? "│ ├── Login.jsx" : null,
319
- ...config.tables.map((t) => `│ ├── ${toPascal(t.name)}.jsx`),
320
- config.needsReports ? "└── Reports.jsx" : null,
329
+ "│ ├── Home.jsx",
330
+ config.needsAuth ? "│ ├── Login.jsx" : null,
331
+ ...config.tables.map((t) => `│ ├── ${toPascal(t.name)}.jsx`),
332
+ config.needsReports ? "│ │ └── Reports.jsx" : null,
321
333
  ].filter(Boolean);
322
334
 
323
335
  const tree = [
@@ -328,9 +340,9 @@ async function showProjectTree(config) {
328
340
  "│ │ ├── db.js",
329
341
  "│ │ └── database.sql",
330
342
  "│ ├── middleware/",
331
- config.needsAuth ? "│ │ └── auth.js" : null,
343
+ config.needsAuth ? "│ │ └── auth.js" : "│ │ (none)",
332
344
  "│ ├── routes/",
333
- ...formatTree(backendRouteFiles).map((line) => `│ ${line}`),
345
+ ...backendRouteFiles,
334
346
  "│ ├── server.js",
335
347
  "│ ├── .env.example",
336
348
  "│ └── package.json",
@@ -339,10 +351,21 @@ async function showProjectTree(config) {
339
351
  " ├── src/",
340
352
  " │ ├── api/",
341
353
  " │ │ └── axios.js",
354
+ config.needsAuth ? " │ ├── context/" : null,
355
+ ...frontendContextFiles,
356
+ config.needsAuth ? " │ ├── components/" : null,
357
+ ...frontendComponentFiles,
342
358
  " │ ├── pages/",
343
- ...formatTree(frontendPageFiles).map((line) => ` │ ${line}`),
359
+ ...frontendPageFiles,
344
360
  " │ ├── App.jsx",
345
- " │ └── main.jsx",
361
+ " │ ├── main.jsx",
362
+ " │ └── index.css",
363
+ " ├── vite.config.js",
364
+ " ├── tailwind.config.js",
365
+ " ├── postcss.config.js",
366
+ " ├── index.html",
367
+ " ├── .env.local.example",
368
+ " ├── .gitignore",
346
369
  " └── package.json",
347
370
  ].filter(Boolean);
348
371
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tss-stack",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
4
4
  "description": "Interactive full-stack Node.js + React + MySQL project generator",
5
5
  "bin": {
6
6
  "tss-stack": "bin/cli.js"
@@ -3,38 +3,38 @@ const path = require("path");
3
3
  const { toPascal, toRoute } = require("./utils");
4
4
 
5
5
  function generateFrontend(config) {
6
- const { projectName, tables, needsAuth, needsReports, targetDir } = config;
7
- const root = path.join(targetDir, "frontend-project");
8
-
9
- fs.outputFileSync(
10
- path.join(root, "package.json"),
11
- JSON.stringify(
12
- {
13
- name: "frontend-project",
14
- version: "1.0.0",
15
- scripts: { dev: "vite", build: "vite build" },
16
- dependencies: {
17
- react: "^18.2.0",
18
- "react-dom": "^18.2.0",
19
- "react-router-dom": "^6.18.0",
20
- axios: "^1.6.0",
21
- },
22
- devDependencies: {
23
- vite: "^5.0.0",
24
- "@vitejs/plugin-react": "^4.2.0",
25
- tailwindcss: "^3.3.0",
26
- autoprefixer: "^10.4.16",
27
- postcss: "^8.4.31",
28
- },
29
- },
30
- null,
31
- 2
32
- )
33
- );
6
+ const { projectName, tables, needsAuth, needsReports, targetDir } = config;
7
+ const root = path.join(targetDir, "frontend-project");
8
+
9
+ fs.outputFileSync(
10
+ path.join(root, "package.json"),
11
+ JSON.stringify(
12
+ {
13
+ name: "frontend-project",
14
+ version: "1.0.0",
15
+ scripts: { dev: "vite", build: "vite build" },
16
+ dependencies: {
17
+ react: "^18.2.0",
18
+ "react-dom": "^18.2.0",
19
+ "react-router-dom": "^6.18.0",
20
+ axios: "^1.6.0",
21
+ },
22
+ devDependencies: {
23
+ vite: "^5.0.0",
24
+ "@vitejs/plugin-react": "^4.2.0",
25
+ tailwindcss: "^3.3.0",
26
+ autoprefixer: "^10.4.16",
27
+ postcss: "^8.4.31",
28
+ },
29
+ },
30
+ null,
31
+ 2
32
+ )
33
+ );
34
34
 
35
- fs.outputFileSync(
36
- path.join(root, "vite.config.js"),
37
- `import { defineConfig } from 'vite';
35
+ fs.outputFileSync(
36
+ path.join(root, "vite.config.js"),
37
+ `import { defineConfig } from 'vite';
38
38
  import react from '@vitejs/plugin-react';
39
39
 
40
40
  export default defineConfig({
@@ -45,11 +45,11 @@ export default defineConfig({
45
45
  },
46
46
  });
47
47
  `
48
- );
48
+ );
49
49
 
50
- fs.outputFileSync(
51
- path.join(root, "tailwind.config.js"),
52
- `export default {
50
+ fs.outputFileSync(
51
+ path.join(root, "tailwind.config.js"),
52
+ `export default {
53
53
  content: [
54
54
  "./index.html",
55
55
  "./src/**/*.{js,jsx}",
@@ -60,22 +60,22 @@ export default defineConfig({
60
60
  plugins: [],
61
61
  };
62
62
  `
63
- );
63
+ );
64
64
 
65
- fs.outputFileSync(
66
- path.join(root, "postcss.config.js"),
67
- `export default {
65
+ fs.outputFileSync(
66
+ path.join(root, "postcss.config.js"),
67
+ `export default {
68
68
  plugins: {
69
69
  tailwindcss: {},
70
70
  autoprefixer: {},
71
71
  },
72
72
  };
73
73
  `
74
- );
74
+ );
75
75
 
76
- fs.outputFileSync(
77
- path.join(root, "index.html"),
78
- `<!DOCTYPE html>
76
+ fs.outputFileSync(
77
+ path.join(root, "index.html"),
78
+ `<!DOCTYPE html>
79
79
  <html lang="en">
80
80
  <head>
81
81
  <meta charset="UTF-8" />
@@ -88,17 +88,17 @@ export default defineConfig({
88
88
  </body>
89
89
  </html>
90
90
  `
91
- );
91
+ );
92
92
 
93
- fs.outputFileSync(
94
- path.join(root, ".env.local.example"),
95
- `VITE_API_URL=http://localhost:5000
93
+ fs.outputFileSync(
94
+ path.join(root, ".env.local.example"),
95
+ `VITE_API_URL=http://localhost:5000
96
96
  `
97
- );
97
+ );
98
98
 
99
- fs.outputFileSync(
100
- path.join(root, ".gitignore"),
101
- `node_modules/
99
+ fs.outputFileSync(
100
+ path.join(root, ".gitignore"),
101
+ `node_modules/
102
102
  dist/
103
103
  .env
104
104
  .env.local
@@ -107,11 +107,11 @@ dist/
107
107
  .idea/
108
108
  .vscode/
109
109
  `
110
- );
110
+ );
111
111
 
112
- fs.outputFileSync(
113
- path.join(root, "src", "api", "axios.js"),
114
- `import axios from "axios";
112
+ fs.outputFileSync(
113
+ path.join(root, "src", "api", "axios.js"),
114
+ `import axios from "axios";
115
115
 
116
116
  const API = axios.create({
117
117
  baseURL: import.meta.env.VITE_API_URL || "http://localhost:5000",
@@ -120,11 +120,11 @@ const API = axios.create({
120
120
 
121
121
  export default API;
122
122
  `
123
- );
123
+ );
124
124
 
125
- fs.outputFileSync(
126
- path.join(root, "src", "main.jsx"),
127
- `import React from "react";
125
+ fs.outputFileSync(
126
+ path.join(root, "src", "main.jsx"),
127
+ `import React from "react";
128
128
  import ReactDOM from "react-dom/client";
129
129
  import App from "./App";
130
130
  import "./index.css";
@@ -135,11 +135,11 @@ ReactDOM.createRoot(document.getElementById("root")).render(
135
135
  </React.StrictMode>
136
136
  );
137
137
  `
138
- );
138
+ );
139
139
 
140
- fs.outputFileSync(
141
- path.join(root, "src", "index.css"),
142
- `@tailwind base;
140
+ fs.outputFileSync(
141
+ path.join(root, "src", "index.css"),
142
+ `@tailwind base;
143
143
  @tailwind components;
144
144
  @tailwind utilities;
145
145
 
@@ -147,12 +147,12 @@ body {
147
147
  font-family: system-ui, -apple-system, sans-serif;
148
148
  }
149
149
  `
150
- );
150
+ );
151
151
 
152
- if (needsAuth) {
153
- fs.outputFileSync(
154
- path.join(root, "src", "context", "AuthContext.jsx"),
155
- `import React, { createContext, useState, useEffect } from "react";
152
+ if (needsAuth) {
153
+ fs.outputFileSync(
154
+ path.join(root, "src", "context", "AuthContext.jsx"),
155
+ `import React, { createContext, useState, useEffect } from "react";
156
156
  import API from "../api/axios";
157
157
 
158
158
  export const AuthContext = createContext();
@@ -182,11 +182,11 @@ export function AuthProvider({ children }) {
182
182
  );
183
183
  }
184
184
  `
185
- );
185
+ );
186
186
 
187
- fs.outputFileSync(
188
- path.join(root, "src", "components", "PrivateRoute.jsx"),
189
- `import { Navigate } from "react-router-dom";
187
+ fs.outputFileSync(
188
+ path.join(root, "src", "components", "PrivateRoute.jsx"),
189
+ `import { Navigate } from "react-router-dom";
190
190
  import { useContext } from "react";
191
191
  import { AuthContext } from "../context/AuthContext";
192
192
 
@@ -197,12 +197,12 @@ export default function PrivateRoute({ children }) {
197
197
  return user ? children : <Navigate to="/login" />;
198
198
  }
199
199
  `
200
- );
201
- }
200
+ );
201
+ }
202
202
 
203
- fs.outputFileSync(
204
- path.join(root, "src", "pages", "Home.jsx"),
205
- `export default function Home() {
203
+ fs.outputFileSync(
204
+ path.join(root, "src", "pages", "Home.jsx"),
205
+ `export default function Home() {
206
206
  return (
207
207
  <div className="p-6 max-w-2xl">
208
208
  <h1 className="text-3xl font-bold mb-4">Welcome</h1>
@@ -211,20 +211,20 @@ export default function PrivateRoute({ children }) {
211
211
  );
212
212
  }
213
213
  `
214
- );
214
+ );
215
215
 
216
- for (const table of tables) {
217
- const name = toPascal(table.name);
218
- const route = toRoute(table.name);
219
- const fields = table.fields;
220
- const ops = table.operations;
221
-
222
- const stateFields = fields.map((f) => ` ${f}: ""`).join(",\n");
223
- const formReset = fields.map((f) => `${f}: ""`).join(", ");
224
- const editSet = fields.map((f) => `${f}: item.${f}`).join(", ");
225
- const inputs = fields
226
- .map(
227
- (f) => ` <input
216
+ for (const table of tables) {
217
+ const name = toPascal(table.name);
218
+ const route = toRoute(table.name);
219
+ const fields = table.fields;
220
+ const ops = table.operations;
221
+
222
+ const stateFields = fields.map((f) => ` ${f}: ""`).join(",\n");
223
+ const formReset = fields.map((f) => `${f}: ""`).join(", ");
224
+ const editSet = fields.map((f) => `${f}: item.${f}`).join(", ");
225
+ const inputs = fields
226
+ .map(
227
+ (f) => ` <input
228
228
  type="text"
229
229
  placeholder="${f}"
230
230
  value={form.${f}}
@@ -232,18 +232,18 @@ export default function PrivateRoute({ children }) {
232
232
  className="border p-2 rounded w-full"
233
233
  required
234
234
  />`
235
- )
236
- .join("\n");
235
+ )
236
+ .join("\n");
237
237
 
238
- const tableHeaders = ["id", ...fields, "created_at"]
239
- .map((f) => ` <th className="border px-4 py-2">${f}</th>`)
240
- .join("\n");
238
+ const tableHeaders = ["id", ...fields, "created_at"]
239
+ .map((f) => ` <th className="border px-4 py-2">${f}</th>`)
240
+ .join("\n");
241
241
 
242
- const tableRow = ["id", ...fields, "created_at"]
243
- .map((f) => ` <td className="border px-4 py-2">{item.${f}}</td>`)
244
- .join("\n");
242
+ const tableRow = ["id", ...fields, "created_at"]
243
+ .map((f) => ` <td className="border px-4 py-2">{item.${f}}</td>`)
244
+ .join("\n");
245
245
 
246
- let page = `import { useState, useEffect } from "react";
246
+ let page = `import { useState, useEffect } from "react";
247
247
  import API from "../api/axios";
248
248
 
249
249
  export default function ${name}() {
@@ -272,8 +272,8 @@ ${ops.includes("update") ? " const [editId, setEditId] = useState(null);" : ""}
272
272
 
273
273
  `;
274
274
 
275
- if (ops.includes("insert")) {
276
- page += ` const handleSubmit = async (e) => {
275
+ if (ops.includes("insert")) {
276
+ page += ` const handleSubmit = async (e) => {
277
277
  e.preventDefault();
278
278
  setLoading(true);
279
279
  setError("");
@@ -298,10 +298,10 @@ ${ops.includes("update") ? ` if (editId) {
298
298
  };
299
299
 
300
300
  `;
301
- }
301
+ }
302
302
 
303
- if (ops.includes("delete")) {
304
- page += ` const handleDelete = async (id) => {
303
+ if (ops.includes("delete")) {
304
+ page += ` const handleDelete = async (id) => {
305
305
  if (!window.confirm("Are you sure?")) return;
306
306
  setLoading(true);
307
307
  setError("");
@@ -317,10 +317,10 @@ ${ops.includes("update") ? ` if (editId) {
317
317
  };
318
318
 
319
319
  `;
320
- }
320
+ }
321
321
 
322
- if (ops.includes("update")) {
323
- page += ` const handleEdit = (item) => {
322
+ if (ops.includes("update")) {
323
+ page += ` const handleEdit = (item) => {
324
324
  setEditId(item.id);
325
325
  setForm({ ${editSet} });
326
326
  };
@@ -331,9 +331,9 @@ ${ops.includes("update") ? ` if (editId) {
331
331
  };
332
332
 
333
333
  `;
334
- }
334
+ }
335
335
 
336
- page += ` return (
336
+ page += ` return (
337
337
  <div className="p-6">
338
338
  <h1 className="text-2xl font-bold mb-4">${name}</h1>
339
339
 
@@ -374,14 +374,14 @@ ${ops.includes("delete") ? ' <button onClick={() => handleDelete(item
374
374
  }
375
375
  `;
376
376
 
377
- fs.outputFileSync(path.join(root, "src", "pages", `${name}.jsx`), page);
378
- console.log(` [✓] pages/${name}.jsx`);
379
- }
377
+ fs.outputFileSync(path.join(root, "src", "pages", `${name}.jsx`), page);
378
+ console.log(` [✓] pages/${name}.jsx`);
379
+ }
380
380
 
381
- if (needsAuth) {
382
- fs.outputFileSync(
383
- path.join(root, "src", "pages", "Login.jsx"),
384
- `import { useState, useContext } from "react";
381
+ if (needsAuth) {
382
+ fs.outputFileSync(
383
+ path.join(root, "src", "pages", "Login.jsx"),
384
+ `import { useState, useContext } from "react";
385
385
  import { useNavigate } from "react-router-dom";
386
386
  import { AuthContext } from "../context/AuthContext";
387
387
  import API from "../api/axios";
@@ -447,13 +447,13 @@ export default function Login() {
447
447
  );
448
448
  }
449
449
  `
450
- );
451
- }
450
+ );
451
+ }
452
452
 
453
- if (needsReports) {
454
- fs.outputFileSync(
455
- path.join(root, "src", "pages", "Reports.jsx"),
456
- `export default function Reports() {
453
+ if (needsReports) {
454
+ fs.outputFileSync(
455
+ path.join(root, "src", "pages", "Reports.jsx"),
456
+ `export default function Reports() {
457
457
  return (
458
458
  <div className="p-6">
459
459
  <h1 className="text-2xl font-bold mb-4">Reports</h1>
@@ -462,27 +462,27 @@ export default function Login() {
462
462
  );
463
463
  }
464
464
  `
465
- );
466
- }
467
-
468
- const imports = tables
469
- .map((t) => `import ${toPascal(t.name)} from "./pages/${toPascal(t.name)}";`)
470
- .join("\n");
471
-
472
- const routes = tables
473
- .map((t) => {
474
- const route = `<Route path="/${toRoute(t.name)}" element={<${toPascal(t.name)} />} />`;
475
- return needsAuth
476
- ? ` <Route path="/${toRoute(t.name)}" element={<PrivateRoute><${toPascal(t.name)} /></PrivateRoute>} />`
477
- : ` ${route}`;
478
- })
479
- .join("\n");
480
-
481
- const navLinks = tables
482
- .map((t) => ` <Link to="/${toRoute(t.name)}" className="hover:underline">${toPascal(t.name)}</Link>`)
483
- .join("\n");
484
-
485
- let app = `import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
465
+ );
466
+ }
467
+
468
+ const imports = tables
469
+ .map((t) => `import ${toPascal(t.name)} from "./pages/${toPascal(t.name)}";`)
470
+ .join("\n");
471
+
472
+ const routes = tables
473
+ .map((t) => {
474
+ const route = `<Route path="/${toRoute(t.name)}" element={<${toPascal(t.name)} />} />`;
475
+ return needsAuth
476
+ ? ` <Route path="/${toRoute(t.name)}" element={<PrivateRoute><${toPascal(t.name)} /></PrivateRoute>} />`
477
+ : ` ${route}`;
478
+ })
479
+ .join("\n");
480
+
481
+ const navLinks = tables
482
+ .map((t) => ` <Link to="/${toRoute(t.name)}" className="hover:underline">${toPascal(t.name)}</Link>`)
483
+ .join("\n");
484
+
485
+ let app = `import { BrowserRouter, Routes, Route, Link, useNavigate } from "react-router-dom";
486
486
  ${imports}
487
487
  ${needsAuth ? 'import Login from "./pages/Login";\nimport PrivateRoute from "./components/PrivateRoute";\nimport { AuthContext, AuthProvider } from "./context/AuthContext";\nimport { useContext } from "react";' : ""}
488
488
  ${needsReports ? 'import Reports from "./pages/Reports";' : ""}
@@ -534,8 +534,8 @@ ${needsAuth ? ' <AuthProvider>\n <BrowserRouter>\n <AppRoutes />\
534
534
  }
535
535
  `;
536
536
 
537
- fs.outputFileSync(path.join(root, "src", "App.jsx"), app);
538
- console.log(" [✓] App.jsx");
537
+ fs.outputFileSync(path.join(root, "src", "App.jsx"), app);
538
+ console.log(" [✓] App.jsx");
539
539
  }
540
540
 
541
541
  module.exports = { generateFrontend };