@erwininteractive/mvc 0.7.1 → 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
|
-
- **
|
|
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
|
|
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
|
-
|
|
141
|
-
|
|
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
|
-
|
|
145
|
-
const
|
|
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
|
|
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, "
|
|
162
|
-
|
|
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.
|
|
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.
|
|
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": {
|