@erwininteractive/mvc 0.4.2 → 0.6.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
@@ -279,6 +279,24 @@ app.get("/protected", authenticate, (req, res) => {
279
279
  });
280
280
  ```
281
281
 
282
+ ### Auto-Inject User to Views
283
+
284
+ When a user is authenticated, their information is automatically available in all EJS templates via `res.locals.user`:
285
+
286
+ ```typescript
287
+ // In any EJS template
288
+ <% if (user) { %>
289
+ <p>Welcome, <%= user.email %></p>
290
+ <% } else { %>
291
+ <a href="/auth/login">Login</a>
292
+ <% } %>
293
+ ```
294
+
295
+ The middleware automatically:
296
+ - Extracts JWT from `req.cookies.token` or `Authorization` header
297
+ - Verifies the token using `JWT_SECRET`
298
+ - Sets `req.user` and `res.locals.user` for use in views
299
+
282
300
  ---
283
301
 
284
302
  ### WebAuthn (Passkeys)
@@ -528,6 +546,50 @@ NODE_ENV=development # Environment
528
546
 
529
547
  ---
530
548
 
549
+ ## Escape Hatch Documentation
550
+
551
+ ### Island Pattern (EJS + React)
552
+
553
+ Use EJS for layout while adding React components where needed:
554
+
555
+ ```html
556
+ <!-- layout.ejs -->
557
+ <!DOCTYPE html>
558
+ <html>
559
+ <head><title><%= title %></title></head>
560
+ <body>
561
+ <header><%- include('partial/header') %></header>
562
+
563
+ <main id="app"><%- body %></main>
564
+
565
+ <!-- React island -->
566
+ <div id="comment-section" data-post-id="<%= post.id %>"></div>
567
+
568
+ <footer><%- include('partial/footer') %></footer>
569
+
570
+ <script type="module" src="/src/components/CommentSection.tsx"></script>
571
+ </body>
572
+ </html>
573
+ ```
574
+
575
+ ### API-Only Mode
576
+
577
+ Disable EJS engine for pure API responses:
578
+
579
+ ```typescript
580
+ const { app } = await createMvcApp({
581
+ viewsPath: "src/views",
582
+ disableViewEngine: true, // Don't setup EJS
583
+ });
584
+
585
+ app.get("/api/users", async (req, res) => {
586
+ const users = await prisma.user.findMany();
587
+ res.json(users); // Always JSON, no EJS
588
+ });
589
+ ```
590
+
591
+ ---
592
+
531
593
  ## Learn More
532
594
 
533
595
  - [Express.js Documentation](https://expressjs.com/)
package/dist/cli.js CHANGED
@@ -7,11 +7,19 @@ const generateModel_1 = require("./generators/generateModel");
7
7
  const generateController_1 = require("./generators/generateController");
8
8
  const generateResource_1 = require("./generators/generateResource");
9
9
  const generateWebAuthn_1 = require("./generators/generateWebAuthn");
10
+ const listRoutes_1 = require("./generators/listRoutes");
10
11
  const program = new commander_1.Command();
11
12
  program
12
13
  .name("erwinmvc")
13
14
  .description("CLI for @erwininteractive/mvc framework")
14
- .version("0.2.0");
15
+ .version("0.2.0")
16
+ .addHelpText("after", `
17
+ Examples:
18
+ $ erwinmvc init myapp Create a new app
19
+ $ erwinmvc generate resource Post Generate CRUD for Post
20
+ $ erwinmvc webauthn Setup passkey authentication
21
+ $ erwinmvc list:routes Show all routes
22
+ `);
15
23
  // Init command - scaffold a new application
16
24
  program
17
25
  .command("init <dir>")
@@ -94,4 +102,18 @@ program
94
102
  process.exit(1);
95
103
  }
96
104
  });
105
+ // List routes command - shows all defined routes
106
+ program
107
+ .command("list:routes")
108
+ .alias("lr")
109
+ .description("List all routes in the application")
110
+ .action(async () => {
111
+ try {
112
+ (0, listRoutes_1.listRoutes)();
113
+ }
114
+ catch (err) {
115
+ console.error("Error:", err instanceof Error ? err.message : err);
116
+ process.exit(1);
117
+ }
118
+ });
97
119
  program.parse();
@@ -12,6 +12,7 @@ const redis_1 = require("redis");
12
12
  const connect_redis_1 = __importDefault(require("connect-redis"));
13
13
  const helmet_1 = __importDefault(require("helmet"));
14
14
  const cors_1 = __importDefault(require("cors"));
15
+ const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
15
16
  const dotenv_1 = __importDefault(require("dotenv"));
16
17
  const path_1 = __importDefault(require("path"));
17
18
  // Load environment variables
@@ -74,6 +75,21 @@ async function createMvcApp(options = {}) {
74
75
  // View engine
75
76
  app.set("view engine", "ejs");
76
77
  app.set("views", path_1.default.resolve(viewsPath));
78
+ // Auto-inject authenticated user into views via res.locals
79
+ app.use((req, res, next) => {
80
+ try {
81
+ const token = req.cookies?.token || req.headers.authorization?.split(" ")[1];
82
+ if (token) {
83
+ const decoded = jsonwebtoken_1.default.verify(token, process.env.JWT_SECRET);
84
+ req.user = decoded;
85
+ res.locals.user = decoded;
86
+ }
87
+ }
88
+ catch {
89
+ // Token invalid or expired
90
+ }
91
+ next();
92
+ });
77
93
  // Add respond helper to response
78
94
  app.use((req, res, next) => {
79
95
  res.respond = function (viewName, data) {
@@ -0,0 +1,22 @@
1
+ interface RouteInfo {
2
+ method: string;
3
+ path: string;
4
+ handler: string;
5
+ }
6
+ /**
7
+ * Parse routes from server.ts file
8
+ */
9
+ export declare function parseRoutes(serverPath: string): RouteInfo[];
10
+ /**
11
+ * Format routes as a table
12
+ */
13
+ export declare function formatRoutes(routes: RouteInfo[]): string;
14
+ /**
15
+ * Get server.ts path from current directory
16
+ */
17
+ export declare function getServerPath(cwd?: string): string;
18
+ /**
19
+ * List all routes in the current project
20
+ */
21
+ export declare function listRoutes(cwd?: string): void;
22
+ export {};
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.parseRoutes = parseRoutes;
7
+ exports.formatRoutes = formatRoutes;
8
+ exports.getServerPath = getServerPath;
9
+ exports.listRoutes = listRoutes;
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const path_1 = __importDefault(require("path"));
12
+ /**
13
+ * Parse routes from server.ts file
14
+ */
15
+ function parseRoutes(serverPath) {
16
+ const routes = [];
17
+ if (!fs_1.default.existsSync(serverPath)) {
18
+ return routes;
19
+ }
20
+ const content = fs_1.default.readFileSync(serverPath, "utf-8");
21
+ const lines = content.split("\n");
22
+ for (const line of lines) {
23
+ // Match app.METHOD("/path", handler)
24
+ const routeMatch = line.match(/app\.(\w+)\s*\(\s*["']([^"']+)["']/);
25
+ if (routeMatch) {
26
+ const currentMethod = routeMatch[1].toLowerCase();
27
+ const currentPath = routeMatch[2];
28
+ let currentHandler = "";
29
+ // Extract handler name - look for arrow function or identifier
30
+ const arrowFuncMatch = line.match(/=>\s*\{/);
31
+ if (arrowFuncMatch) {
32
+ currentHandler = "<anonymous>";
33
+ }
34
+ else {
35
+ const handlerMatch = line.match(/,\s*([^\s,)]+)/);
36
+ if (handlerMatch) {
37
+ currentHandler = handlerMatch[1].trim();
38
+ }
39
+ else {
40
+ currentHandler = "<anonymous>";
41
+ }
42
+ }
43
+ routes.push({
44
+ method: currentMethod.toUpperCase(),
45
+ path: currentPath,
46
+ handler: currentHandler,
47
+ });
48
+ }
49
+ }
50
+ return routes;
51
+ }
52
+ /**
53
+ * Format routes as a table
54
+ */
55
+ function formatRoutes(routes) {
56
+ if (routes.length === 0) {
57
+ return "No routes found.";
58
+ }
59
+ const headers = ["Method", "Path", "Handler"];
60
+ const colWidths = [6, 20, 30];
61
+ const formatRow = (cells) => {
62
+ return cells.map((cell, i) => cell.padEnd(colWidths[i])).join(" | ");
63
+ };
64
+ const separator = colWidths.map(w => "-".repeat(w)).join("-+-");
65
+ let output = "";
66
+ output += formatRow(headers) + "\n";
67
+ output += separator + "\n";
68
+ for (const route of routes) {
69
+ output += formatRow([
70
+ route.method,
71
+ route.path,
72
+ route.handler,
73
+ ]) + "\n";
74
+ }
75
+ return output;
76
+ }
77
+ /**
78
+ * Get server.ts path from current directory
79
+ */
80
+ function getServerPath(cwd = process.cwd()) {
81
+ const possiblePaths = [
82
+ path_1.default.join(cwd, "src", "server.ts"),
83
+ path_1.default.join(cwd, "server.ts"),
84
+ ];
85
+ for (const p of possiblePaths) {
86
+ if (fs_1.default.existsSync(p)) {
87
+ return p;
88
+ }
89
+ }
90
+ return possiblePaths[0];
91
+ }
92
+ /**
93
+ * List all routes in the current project
94
+ */
95
+ function listRoutes(cwd = process.cwd()) {
96
+ const serverPath = getServerPath(cwd);
97
+ const routes = parseRoutes(serverPath);
98
+ console.log("\nDefined Routes:\n");
99
+ console.log(formatRoutes(routes));
100
+ if (routes.length === 0) {
101
+ console.log("\nNo routes found. Add routes to src/server.ts");
102
+ }
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erwininteractive/mvc",
3
- "version": "0.4.2",
3
+ "version": "0.6.1",
4
4
  "description": "A lightweight, full-featured MVC framework for Node.js with Express, Prisma, and EJS",
5
5
  "main": "dist/framework/index.js",
6
6
  "types": "dist/framework/index.d.ts",
@@ -76,6 +76,8 @@ Create `.ejs` files in `src/views/`. EJS lets you use JavaScript in your HTML:
76
76
  <%- include('partials/header') %>
77
77
  ```
78
78
 
79
+ **Note:** When using authentication, the `user` object is automatically available in all views when a user is logged in. No need to pass it manually!
80
+
79
81
  ### Adding Routes
80
82
 
81
83
  Edit `src/server.ts` to add routes:
@@ -201,6 +203,32 @@ Then run migrations again:
201
203
  npx prisma migrate dev --name add-post-fields
202
204
  ```
203
205
 
206
+ ## Full-Stack Flexibility
207
+
208
+ ### Island Pattern (EJS + React)
209
+
210
+ Use EJS for layout while adding React components where needed:
211
+
212
+ ```html
213
+ <div id="comment-section" data-post-id="<%= post.id %>"></div>
214
+ <script type="module" src="/src/components/CommentSection.tsx"></script>
215
+ ```
216
+
217
+ ### API-Only Mode
218
+
219
+ For pure API development, disable EJS:
220
+
221
+ ```typescript
222
+ const { app } = await createMvcApp({
223
+ viewsPath: "src/views",
224
+ disableViewEngine: true,
225
+ });
226
+
227
+ app.get("/api/users", async (req, res) => {
228
+ res.json(users); // Always JSON
229
+ });
230
+ ```
231
+
204
232
  ### Step 5: Use in Your Code
205
233
 
206
234
  ```typescript
@@ -284,10 +312,22 @@ Link to them in your templates:
284
312
 
285
313
  | Command | Description |
286
314
  |---------|-------------|
287
- | `npx erwinmvc generate controller <Name>` | Create a CRUD controller |
288
- | `npx erwinmvc generate model <Name>` | Create a database model |
289
-
290
- ---
315
+ | `npx @erwininteractive/mvc init <dir>` | Create a new app |
316
+ | `npx erwinmvc generate resource <name>` | Generate model + controller + views |
317
+ | `npx erwinmvc generate controller <name>` | Generate a CRUD controller |
318
+ | `npx erwinmvc generate model <name>` | Generate a database model |
319
+ | `npx erwinmvc webauthn` | Generate WebAuthn authentication (security keys) |
320
+ | `npx erwinmvc list:routes` | Show all defined routes |
321
+
322
+ ### Init Options
323
+
324
+ | Option | Description |
325
+ |--------|-------------|
326
+ | `--skip-install` | Skip running npm install |
327
+ | `--with-database` | Include Prisma database setup |
328
+ | `--with-ci` | Include GitHub Actions CI workflow |
329
+
330
+ ### Resource Options
291
331
 
292
332
  ## Learn More
293
333
 
@@ -12,9 +12,11 @@
12
12
  "db:push": "prisma db push"
13
13
  },
14
14
  "dependencies": {
15
- "@erwininteractive/mvc": "^1.0.0"
15
+ "@erwininteractive/mvc": "^0.4.0",
16
+ "cookie-parser": "^1.4.6"
16
17
  },
17
18
  "devDependencies": {
19
+ "@types/cookie-parser": "^1.4.7",
18
20
  "@types/express": "^5.0.0",
19
21
  "@types/node": "^22.7.5",
20
22
  "tsx": "^4.19.1",
@@ -1,4 +1,5 @@
1
1
  import { createMvcApp, startServer } from "@erwininteractive/mvc";
2
+ import cookieParser from "cookie-parser";
2
3
 
3
4
  async function main() {
4
5
  const { app } = await createMvcApp({
@@ -6,8 +7,11 @@ async function main() {
6
7
  publicPath: "public",
7
8
  });
8
9
 
10
+ // Parse cookies (needed for JWT authentication)
11
+ app.use(cookieParser());
12
+
9
13
  // Root route - displays welcome page
10
- app.get("/", (req, res) => {
14
+ app.get("/", (req: any, res: any) => {
11
15
  res.render("index", { title: "Welcome" });
12
16
  });
13
17