@erwininteractive/mvc 0.6.7 → 0.8.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
@@ -10,7 +10,9 @@ A lightweight, full-featured MVC framework for Node.js 20+ built with TypeScript
10
10
  - **Optional Redis Sessions** - Scalable session management
11
11
  - **JWT Authentication** - Secure token-based auth with bcrypt password hashing
12
12
  - **WebAuthn (Passkeys)** - Passwordless authentication with security keys (YubiKey, Touch ID, Face ID)
13
- - **CLI Tools** - Scaffold apps and generate models/controllers
13
+ - **Tailwind CSS** - Modern, utility-first CSS framework (optional, included with `--with-tailwind`)
14
+ - **Zod Validation** - Type-safe form validation with TypeScript-first schemas
15
+ - **CLI Tools** - Scaffold apps with `--with-tailwind`, generate models/controllers/routes
14
16
 
15
17
  ## Quick Start
16
18
 
@@ -23,580 +25,3 @@ npm run dev
23
25
  Visit http://localhost:3000 - your app is running!
24
26
 
25
27
  ---
26
-
27
- ## Getting Started
28
-
29
- ### Step 1: Create a New Page
30
-
31
- Create `src/views/about.ejs`:
32
-
33
- ```html
34
- <!doctype html>
35
- <html>
36
- <head>
37
- <title><%= title %></title>
38
- </head>
39
- <body>
40
- <h1><%= title %></h1>
41
- <p>Welcome to my about page!</p>
42
- </body>
43
- </html>
44
- ```
45
-
46
- ### Step 2: Add a Route
47
-
48
- Edit `src/server.ts`:
49
-
50
- ```typescript
51
- app.get("/about", (req, res) => {
52
- res.render("about", { title: "About Us" });
53
- });
54
- ```
55
-
56
- ### Step 3: View Your Page
57
-
58
- Visit http://localhost:3000/about
59
-
60
- ---
61
-
62
- ## Creating Pages
63
-
64
- ### EJS Templates
65
-
66
- Create `.ejs` files in `src/views/`. EJS lets you use JavaScript in HTML:
67
-
68
- ```html
69
- <!-- Output a variable (escaped) -->
70
- <h1><%= title %></h1>
71
-
72
- <!-- Output raw HTML -->
73
- <%- htmlContent %>
74
-
75
- <!-- JavaScript logic -->
76
- <% if (user) { %>
77
- <p>Welcome, <%= user.name %>!</p>
78
- <% } %>
79
-
80
- <!-- Loop through items -->
81
- <ul>
82
- <% items.forEach(item => { %>
83
- <li><%= item.name %></li>
84
- <% }); %>
85
- </ul>
86
-
87
- <!-- Include another template -->
88
- <%- include('partials/header') %>
89
- ```
90
-
91
- ### Adding Routes
92
-
93
- ```typescript
94
- // Simple page
95
- app.get("/contact", (req, res) => {
96
- res.render("contact", { title: "Contact Us" });
97
- });
98
-
99
- // Handle form submission
100
- app.post("/contact", (req, res) => {
101
- const { name, email, message } = req.body;
102
- console.log(`Message from ${name}: ${message}`);
103
- res.redirect("/contact?sent=true");
104
- });
105
-
106
- // JSON API endpoint
107
- app.get("/api/users", (req, res) => {
108
- res.json([{ id: 1, name: "John" }]);
109
- });
110
- ```
111
-
112
- ---
113
-
114
- ## Resources
115
-
116
- Generate a complete resource (model + controller + views) with one command:
117
-
118
- ```bash
119
- npx erwinmvc generate resource Post
120
- ```
121
-
122
- This creates:
123
- - `prisma/schema.prisma` - Adds the Post model
124
- - `src/controllers/PostController.ts` - Full CRUD controller with form handling
125
- - `src/views/posts/index.ejs` - List view
126
- - `src/views/posts/show.ejs` - Detail view
127
- - `src/views/posts/create.ejs` - Create form
128
- - `src/views/posts/edit.ejs` - Edit form
129
-
130
- ### Resource Routes
131
-
132
- | Action | HTTP Method | URL | Description |
133
- |-----------|-------------|------------------|------------------|
134
- | `index` | GET | /posts | List all |
135
- | `create` | GET | /posts/create | Show create form |
136
- | `store` | POST | /posts | Create new |
137
- | `show` | GET | /posts/:id | Show one |
138
- | `edit` | GET | /posts/:id/edit | Show edit form |
139
- | `update` | PUT | /posts/:id | Update |
140
- | `destroy` | DELETE | /posts/:id | Delete |
141
-
142
- ### Wiring Up Routes
143
-
144
- Add to `src/server.ts`:
145
-
146
- ```typescript
147
- import * as PostController from "./controllers/PostController";
148
-
149
- app.get("/posts", PostController.index);
150
- app.get("/posts/create", PostController.create);
151
- app.post("/posts", PostController.store);
152
- app.get("/posts/:id", PostController.show);
153
- app.get("/posts/:id/edit", PostController.edit);
154
- app.put("/posts/:id", PostController.update);
155
- app.delete("/posts/:id", PostController.destroy);
156
- ```
157
-
158
- ---
159
-
160
- ## Controllers
161
-
162
- Generate just a controller (without model/views):
163
-
164
- ```bash
165
- npx erwinmvc generate controller Product
166
- ```
167
-
168
- This creates `src/controllers/ProductController.ts` with CRUD actions:
169
-
170
- | Action | HTTP Method | URL | Description |
171
- |-----------|-------------|------------------|-------------|
172
- | `index` | GET | /products | List all |
173
- | `show` | GET | /products/:id | Show one |
174
- | `store` | POST | /products | Create |
175
- | `update` | PUT | /products/:id | Update |
176
- | `destroy` | DELETE | /products/:id | Delete |
177
-
178
- ### Using Controllers
179
-
180
- ```typescript
181
- import * as ProductController from "./controllers/ProductController";
182
-
183
- app.get("/products", ProductController.index);
184
- app.get("/products/:id", ProductController.show);
185
- app.post("/products", ProductController.store);
186
- app.put("/products/:id", ProductController.update);
187
- app.delete("/products/:id", ProductController.destroy);
188
- ```
189
-
190
- ---
191
-
192
- ## Database (Optional)
193
-
194
- Your app works without a database. Add one when you need it.
195
-
196
- ### Setup
197
-
198
- ```bash
199
- npm run db:setup
200
- ```
201
-
202
- Edit `.env` with your database URL:
203
-
204
- ```
205
- DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
206
- ```
207
-
208
- Run migrations:
209
-
210
- ```bash
211
- npx prisma migrate dev --name init
212
- ```
213
-
214
- ### Generate Models
215
-
216
- ```bash
217
- npx erwinmvc generate model Post
218
- ```
219
-
220
- Edit `prisma/schema.prisma` to add fields:
221
-
222
- ```prisma
223
- model Post {
224
- id Int @id @default(autoincrement())
225
- title String
226
- content String?
227
- published Boolean @default(false)
228
- createdAt DateTime @default(now())
229
- updatedAt DateTime @updatedAt
230
-
231
- @@map("posts")
232
- }
233
- ```
234
-
235
- Run migrations again:
236
-
237
- ```bash
238
- npx prisma migrate dev --name add-post-fields
239
- ```
240
-
241
- ### Use in Code
242
-
243
- ```typescript
244
- import { getPrismaClient } from "@erwininteractive/mvc";
245
-
246
- const prisma = getPrismaClient();
247
-
248
- app.get("/posts", async (req, res) => {
249
- const posts = await prisma.post.findMany();
250
- res.render("posts/index", { posts });
251
- });
252
- ```
253
-
254
- ---
255
-
256
- ## Authentication
257
-
258
- ```typescript
259
- import {
260
- hashPassword,
261
- verifyPassword,
262
- signToken,
263
- verifyToken,
264
- authenticate,
265
- } from "@erwininteractive/mvc";
266
-
267
- // Hash a password
268
- const hash = await hashPassword("secret123");
269
-
270
- // Verify a password
271
- const isValid = await verifyPassword("secret123", hash);
272
-
273
- // Sign a JWT
274
- const token = signToken({ userId: 1, email: "user@example.com" });
275
-
276
- // Protect routes with middleware
277
- app.get("/protected", authenticate, (req, res) => {
278
- res.json({ user: req.user });
279
- });
280
- ```
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
-
300
- ---
301
-
302
- ### WebAuthn (Passkeys)
303
-
304
- Passwordless authentication with security keys:
305
-
306
- ```bash
307
- npx erwinmvc webauthn
308
- ```
309
-
310
- This generates:
311
- - `src/controllers/WebAuthnController.ts` - Registration and login handlers
312
- - `src/views/webauthn/` - EJS views for register/login pages
313
- - Prisma `WebAuthnCredential` model for storing security key data
314
-
315
- ```typescript
316
- import {
317
- startRegistration,
318
- completeRegistration,
319
- startAuthentication,
320
- completeAuthentication,
321
- getRPConfig,
322
- } from "@erwininteractive/mvc";
323
-
324
- const { rpID, rpName } = getRPConfig();
325
-
326
- const options = await startRegistration(req, res);
327
- await completeRegistration(req, res);
328
-
329
- const options = await startAuthentication(req, res);
330
- await completeAuthentication(req, res);
331
- ```
332
-
333
- Environment variables:
334
- - `WEBAUTHN_RP_ID` - Your domain (e.g., "localhost" or "example.com")
335
- - `WEBAUTHN_RP_NAME` - Your app name (e.g., "ErwinMVC App")
336
-
337
- Note: WebAuthn requires HTTPS or localhost.
338
-
339
- ---
340
-
341
- ## CLI Commands
342
-
343
- | Command | Description |
344
- |---------|-------------|
345
- | `npx @erwininteractive/mvc init <dir>` | Create a new app |
346
- | `npx erwinmvc generate resource <name>` | Generate model + controller + views |
347
- | `npx erwinmvc generate controller <name>` | Generate a CRUD controller |
348
- | `npx erwinmvc generate model <name>` | Generate a database model |
349
- | `npx erwinmvc webauthn` | Generate WebAuthn authentication (security keys) |
350
-
351
- ### Init Options
352
-
353
- | Option | Description |
354
- |--------|-------------|
355
- | `--skip-install` | Skip running npm install |
356
- | `--with-database` | Include Prisma database setup |
357
- | `--with-ci` | Include GitHub Actions CI workflow |
358
-
359
- ### Resource Options
360
-
361
- | Option | Description |
362
- |--------|-------------|
363
- | `--skip-model` | Skip generating Prisma model |
364
- | `--skip-controller` | Skip generating controller |
365
- | `--skip-views` | Skip generating views |
366
- | `--skip-migrate` | Skip running Prisma migrate |
367
- | `--api-only` | Generate API-only controller (no views) |
368
-
369
- ### Other Generate Options
370
-
371
- | Option | Description |
372
- |--------|-------------|
373
- | `--skip-migrate` | Skip running Prisma migrate (model) |
374
- | `--no-views` | Skip generating EJS views (controller) |
375
-
376
- ---
377
-
378
- ## Project Structure
379
-
380
- ```
381
- myapp/
382
- ├── src/
383
- │ ├── server.ts # Main app - add routes here
384
- │ ├── views/ # EJS templates
385
- │ ├── controllers/ # Route handlers (optional)
386
- │ └── middleware/ # Express middleware (optional)
387
- ├── public/ # Static files (CSS, JS, images)
388
- ├── prisma/ # Database (after db:setup)
389
- │ └── schema.prisma
390
- ├── .env.example
391
- ├── .gitignore
392
- ├── package.json
393
- └── tsconfig.json
394
- ```
395
-
396
- ### Static Files
397
-
398
- Files in `public/` are served at the root URL:
399
-
400
- ```
401
- public/css/style.css → /css/style.css
402
- public/images/logo.png → /images/logo.png
403
- ```
404
-
405
- ---
406
-
407
- ## App Commands
408
-
409
- | Command | Description |
410
- |---------|-------------|
411
- | `npm run dev` | Start development server (auto-reload) |
412
- | `npm run build` | Build for production |
413
- | `npm start` | Run production build |
414
- | `npm run db:setup` | Install database dependencies |
415
- | `npm run db:migrate` | Run database migrations |
416
-
417
- ---
418
-
419
- ## CI/CD (Optional)
420
-
421
- Add GitHub Actions CI to your project for automated testing:
422
-
423
- ```bash
424
- npx @erwininteractive/mvc init myapp --with-ci
425
- ```
426
-
427
- Or add CI to an existing project by creating `.github/workflows/test.yml`:
428
-
429
- ```yaml
430
- name: Test
431
-
432
- on:
433
- push:
434
- branches: [main]
435
- pull_request:
436
- branches: [main]
437
-
438
- jobs:
439
- test:
440
- runs-on: ubuntu-latest
441
-
442
- steps:
443
- - name: Checkout
444
- uses: actions/checkout@v4
445
-
446
- - name: Setup Node.js
447
- uses: actions/setup-node@v4
448
- with:
449
- node-version: "20"
450
- cache: "npm"
451
-
452
- - name: Install dependencies
453
- run: npm ci
454
-
455
- - name: Run tests
456
- run: npm test
457
-
458
- - name: Build
459
- run: npm run build
460
- ```
461
-
462
- ### Adding Database Tests
463
-
464
- If your app uses a database, add PostgreSQL as a service:
465
-
466
- ```yaml
467
- jobs:
468
- test:
469
- runs-on: ubuntu-latest
470
-
471
- services:
472
- postgres:
473
- image: postgres:16
474
- env:
475
- POSTGRES_USER: postgres
476
- POSTGRES_PASSWORD: postgres
477
- POSTGRES_DB: test
478
- ports:
479
- - 5432:5432
480
- options: >-
481
- --health-cmd pg_isready
482
- --health-interval 10s
483
- --health-timeout 5s
484
- --health-retries 5
485
-
486
- steps:
487
- - name: Checkout
488
- uses: actions/checkout@v4
489
-
490
- - name: Setup Node.js
491
- uses: actions/setup-node@v4
492
- with:
493
- node-version: "20"
494
- cache: "npm"
495
-
496
- - name: Install dependencies
497
- run: npm ci
498
-
499
- - name: Run migrations
500
- run: npx prisma migrate deploy
501
- env:
502
- DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
503
-
504
- - name: Run tests
505
- run: npm test
506
- env:
507
- DATABASE_URL: postgresql://postgres:postgres@localhost:5432/test
508
-
509
- - name: Build
510
- run: npm run build
511
- ```
512
-
513
- ### Secrets
514
-
515
- For production deployments, add these secrets in your GitHub repository settings:
516
-
517
- | Secret | Description |
518
- |--------|-------------|
519
- | `DATABASE_URL` | Production database connection string |
520
- | `REDIS_URL` | Production Redis connection string |
521
- | `JWT_SECRET` | Secret key for JWT signing |
522
- | `SESSION_SECRET` | Secret key for session encryption |
523
-
524
- Access secrets in your workflow:
525
-
526
- ```yaml
527
- env:
528
- DATABASE_URL: ${{ secrets.DATABASE_URL }}
529
- JWT_SECRET: ${{ secrets.JWT_SECRET }}
530
- ```
531
-
532
- ---
533
-
534
- ## Environment Variables
535
-
536
- All optional. Create `.env` from `.env.example`:
537
-
538
- ```env
539
- DATABASE_URL="postgresql://user:pass@localhost:5432/mydb" # For database
540
- REDIS_URL="redis://localhost:6379" # For sessions
541
- JWT_SECRET="your-secret-key" # For auth
542
- SESSION_SECRET="your-session-secret" # For sessions
543
- PORT=3000 # Server port
544
- NODE_ENV=development # Environment
545
- ```
546
-
547
- ---
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
-
593
- ## Learn More
594
-
595
- - [Express.js Documentation](https://expressjs.com/)
596
- - [EJS Documentation](https://ejs.co/)
597
- - [Prisma Documentation](https://www.prisma.io/docs/)
598
- - [Alpine.js Documentation](https://alpinejs.dev/)
599
-
600
- ## License
601
-
602
- MIT
@@ -131,48 +131,38 @@ function setupDatabase(targetDir) {
131
131
  }
132
132
  }
133
133
  /**
134
- * Setup Tailwind CSS with PostCSS.
134
+ * Setup Tailwind CSS with pre-built CSS.
135
135
  */
136
136
  function setupTailwind(targetDir) {
137
137
  console.log("\nSetting up Tailwind CSS...");
138
138
  try {
139
139
  (0, child_process_1.execSync)("npm install -D tailwindcss postcss autoprefixer", { cwd: targetDir, stdio: "inherit" });
140
- // Removed npx command - using manual config
141
- // Create assets directory
140
+ const templateDir = (0, paths_1.getTemplatesDir)();
141
+ const templateCss = path_1.default.join(templateDir, "appScaffold", "public", "dist", "tailwind.css");
142
+ const targetCss = path_1.default.join(targetDir, "public", "dist", "tailwind.css");
143
+ fs_1.default.mkdirSync(path_1.default.dirname(targetCss), { recursive: true });
144
+ fs_1.default.copyFileSync(templateCss, targetCss);
142
145
  const assetsDir = path_1.default.join(targetDir, "src", "assets");
143
146
  fs_1.default.mkdirSync(assetsDir, { recursive: true });
144
- // Create tailwind.css
145
- const tailwindCss = `@tailwind base;
146
- @tailwind components;
147
- @tailwind utilities;
148
- `;
149
- fs_1.default.writeFileSync(path_1.default.join(assetsDir, "tailwind.css"), tailwindCss);
150
- // Update postcss.config.cjs
151
- const postcssConfig = `/** @type {import('postcss-load-config').Config} */
152
- const config = {
153
- plugins: {
154
- tailwindcss: {},
155
- autoprefixer: {},
156
- },
157
- };
147
+ fs_1.default.writeFileSync(path_1.default.join(assetsDir, "tailwind.css"), `@import "tailwindcss";\n`);
148
+ const config = `import { type Config } from "tailwindcss";
158
149
 
159
- export default config;
150
+ export default {
151
+ content: ["./src/**/*.{js,ts,tsx}"],
152
+ theme: { extend: {} },
153
+ plugins: [],
154
+ } satisfies Config;
160
155
  `;
161
- fs_1.default.writeFileSync(path_1.default.join(targetDir, "postcss.config.cjs"), postcssConfig);
162
- // Update package.json with tailwind build script
163
- const packageJsonPath = path_1.default.join(targetDir, "package.json");
164
- if (fs_1.default.existsSync(packageJsonPath)) {
165
- const pkg = JSON.parse(fs_1.default.readFileSync(packageJsonPath, "utf-8"));
166
- pkg.scripts.tailwind = "tailwindcss -i ./src/assets/tailwind.css -o ./public/dist/tailwind.css --watch";
167
- pkg.scripts.build = "tsc && npm run tailwind";
168
- fs_1.default.writeFileSync(packageJsonPath, JSON.stringify(pkg, null, 2));
169
- console.log("✓ Added Tailwind CSS support");
170
- }
156
+ fs_1.default.writeFileSync(path_1.default.join(targetDir, "tailwind.config.ts"), config);
157
+ console.log("✓ Added Tailwind CSS support");
171
158
  }
172
159
  catch {
173
- console.error("Failed to setup Tailwind CSS. Run 'npm install -D tailwindcss postcss autoprefixer' manually.");
160
+ console.error("Failed to setup Tailwind CSS.");
174
161
  }
175
162
  }
163
+ /**
164
+ * Recursively copy a directory.
165
+ */
176
166
  /**
177
167
  * Recursively copy a directory.
178
168
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@erwininteractive/mvc",
3
- "version": "0.6.7",
3
+ "version": "0.8.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",
@@ -51,8 +51,10 @@
51
51
  "express-session": "^1.18.0",
52
52
  "helmet": "^8.0.0",
53
53
  "jsonwebtoken": "^9.0.2",
54
+ "postcss": "^8.4.38",
54
55
  "prisma": "^6.0.0",
55
56
  "redis": "^4.7.0",
57
+ "tailwindcss": "^4.2.2",
56
58
  "zod": "^3.23.8"
57
59
  },
58
60
  "devDependencies": {
@@ -1,3 +1 @@
1
- @tailwind base;
2
- @tailwind components;
3
- @tailwind utilities;
1
+ @import "tailwindcss";
@@ -37,7 +37,7 @@ jobs:
37
37
  cache: "npm"
38
38
 
39
39
  - name: Install dependencies
40
- run: npm ci
40
+ run: npm install
41
41
 
42
42
  - name: Run tests
43
43
  run: npm test