create-aziz-app 3.1.1 → 3.1.3
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 +658 -207
- package/bin/create-aziz-app.js +302 -351
- package/package.json +1 -1
- package/template/README.md +14 -279
- package/template/_framework/api.js +219 -219
- package/template/_framework/boot.js +33 -33
- package/template/_framework/hooks.js +367 -291
- package/template/_framework/next-response.js +128 -128
- package/template/_framework/route-match.js +145 -145
- package/template/_framework/router.js +465 -259
- package/template/_framework/runtime.js +264 -264
- package/template/app/layout.js +3 -5
- package/template/app/page.js +9 -32
- package/template/docs/01-getting-started.md +151 -151
- package/template/docs/02-project-structure.md +109 -109
- package/template/docs/03-pages-and-layouts.md +140 -140
- package/template/docs/04-routing.md +120 -120
- package/template/docs/05-hooks.md +169 -169
- package/template/docs/06-api-routes.md +210 -210
- package/template/docs/07-api-client.md +151 -151
- package/template/docs/08-styling.md +94 -94
- package/template/docs/09-imports-and-modules.md +97 -97
- package/template/docs/10-database-and-models.md +154 -154
- package/template/docs/11-authentication.md +171 -171
- package/template/docs/12-production-and-deployment.md +121 -121
- package/template/docs/13-internals.md +179 -179
- package/template/docs/README.md +67 -67
- package/template/index.html +20 -20
- package/template/package.json +16 -16
- package/template/scripts/dev-server.js +493 -381
- package/template/scripts/lib/api-router.js +525 -525
- package/template/scripts/lib/js-transform.js +213 -141
- package/template/scripts/lib/module-resolver.js +132 -132
|
@@ -1,171 +1,171 @@
|
|
|
1
|
-
# 11 · Authentication
|
|
2
|
-
|
|
3
|
-
There's no built-in auth. Because API routes run in Node and the browser client is flexible, you can add JWT-based, session-based (cookies), or OAuth flows yourself.
|
|
4
|
-
|
|
5
|
-
## Principles
|
|
6
|
-
|
|
7
|
-
- **Don't trust the browser.** Auth must be checked in **API routes** (server), not just hidden in UI.
|
|
8
|
-
- **Protect routes.** Return `401`/`403` from API handlers when unauthorized.
|
|
9
|
-
- **Send credentials in requests.** Use `Authorization: Bearer <token>` or rely on `HttpOnly` cookies.
|
|
10
|
-
- **Keep secrets in `.env`.** Never put `JWT_SECRET` in page/components.
|
|
11
|
-
|
|
12
|
-
## JWT (stateless) — minimal example
|
|
13
|
-
|
|
14
|
-
### 1. Install
|
|
15
|
-
|
|
16
|
-
```bash
|
|
17
|
-
npm install jsonwebtoken
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
### 2. `api/login.js`
|
|
21
|
-
|
|
22
|
-
```js
|
|
23
|
-
// api/login.js
|
|
24
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
25
|
-
const jwt = require('jsonwebtoken');
|
|
26
|
-
|
|
27
|
-
const SECRET = process.env.JWT_SECRET || 'change-me';
|
|
28
|
-
|
|
29
|
-
module.exports = {
|
|
30
|
-
POST: async (request) => {
|
|
31
|
-
const { email, password } = await request.json();
|
|
32
|
-
if (!email || !password) return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
|
33
|
-
|
|
34
|
-
// Fake user check for demo — replace with DB lookup + bcrypt
|
|
35
|
-
if (email === 'demo@example.com' && password === 'demo') {
|
|
36
|
-
const token = jwt.sign({ sub: 'u_123', email }, SECRET, { expiresIn: '7d' });
|
|
37
|
-
return NextResponse.json({ token });
|
|
38
|
-
}
|
|
39
|
-
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### 3. `api/me.js` (protected)
|
|
45
|
-
|
|
46
|
-
```js
|
|
47
|
-
// api/me.js
|
|
48
|
-
const { NextResponse } = require('../_framework/next-response');
|
|
49
|
-
const jwt = require('jsonwebtoken');
|
|
50
|
-
|
|
51
|
-
const SECRET = process.env.JWT_SECRET || 'change-me';
|
|
52
|
-
|
|
53
|
-
function getUser(req) {
|
|
54
|
-
const auth = req.headers.authorization || '';
|
|
55
|
-
if (!auth.startsWith('Bearer ')) return null;
|
|
56
|
-
const token = auth.slice(7);
|
|
57
|
-
try {
|
|
58
|
-
return jwt.verify(token, SECRET);
|
|
59
|
-
} catch {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
module.exports = {
|
|
65
|
-
GET: async (request) => {
|
|
66
|
-
const user = getUser(request);
|
|
67
|
-
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
68
|
-
return NextResponse.json({ user });
|
|
69
|
-
},
|
|
70
|
-
};
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
### 4. Browser usage
|
|
74
|
-
|
|
75
|
-
```js
|
|
76
|
-
async function login() {
|
|
77
|
-
const res = await api.post('/api/login', { email: 'demo@example.com', password: 'demo' });
|
|
78
|
-
localStorage.setItem('token', res.token);
|
|
79
|
-
api.setHeaders({ Authorization: `Bearer ${res.token}` });
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async function me() {
|
|
83
|
-
const res = await api.get('/api/me');
|
|
84
|
-
console.log(res.user);
|
|
85
|
-
}
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
If you prefer not to keep in `localStorage`, have the login endpoint set an **HttpOnly + Secure + SameSite** cookie via `NextResponse.cookies.set` and send it on every request (the browser does this automatically). Then read `request.cookies` on the server.
|
|
89
|
-
|
|
90
|
-
## Sessions with cookies (HttpOnly)
|
|
91
|
-
|
|
92
|
-
```js
|
|
93
|
-
const sessions = new Map(); // in-memory; replace with Redis for multi-instance
|
|
94
|
-
|
|
95
|
-
module.exports.POST = async (request) => {
|
|
96
|
-
const { email, password } = await request.json();
|
|
97
|
-
if (email !== 'demo@example.com' || password !== 'demo') {
|
|
98
|
-
return NextResponse.json({ error: 'Invalid' }, { status: 401 });
|
|
99
|
-
}
|
|
100
|
-
const sid = crypto.randomUUID();
|
|
101
|
-
sessions.set(sid, { user: { id: 1, email } });
|
|
102
|
-
const res = NextResponse.json({ ok: true });
|
|
103
|
-
res.cookies.set('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 86400 });
|
|
104
|
-
return res;
|
|
105
|
-
};
|
|
106
|
-
```
|
|
107
|
-
|
|
108
|
-
Then check `request.cookies.sid` against `sessions`.
|
|
109
|
-
|
|
110
|
-
## Protecting pages (UI guard)
|
|
111
|
-
|
|
112
|
-
You can gate the UI in a page. The server remains the source of truth.
|
|
113
|
-
|
|
114
|
-
```js
|
|
115
|
-
export default function Dashboard() {
|
|
116
|
-
const [ok, setOk] = useState(false);
|
|
117
|
-
|
|
118
|
-
useEffect(() => {
|
|
119
|
-
api.get('/api/me')
|
|
120
|
-
.then(() => setOk(true))
|
|
121
|
-
.catch(() => navigate('/login'));
|
|
122
|
-
}, []);
|
|
123
|
-
|
|
124
|
-
if (!ok) return <p>Checking auth…</p>;
|
|
125
|
-
return <h1>Dashboard</h1>;
|
|
126
|
-
}
|
|
127
|
-
```
|
|
128
|
-
|
|
129
|
-
## Protecting with roles
|
|
130
|
-
|
|
131
|
-
Attach a `role` to the token/session and check it:
|
|
132
|
-
|
|
133
|
-
```js
|
|
134
|
-
function requireRole(role) {
|
|
135
|
-
return async (request) => {
|
|
136
|
-
const user = getUser(request);
|
|
137
|
-
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
138
|
-
if (user.role !== role) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
139
|
-
return NextResponse.json({ ok: true, user });
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
module.exports.GET = requireRole('admin');
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
## Passwords
|
|
147
|
-
|
|
148
|
-
**Never** compare plain text. Use `bcrypt` or `argon2`:
|
|
149
|
-
|
|
150
|
-
```bash
|
|
151
|
-
npm install bcryptjs
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
```js
|
|
155
|
-
const bcrypt = require('bcryptjs');
|
|
156
|
-
const hash = await bcrypt.hash(password, 10);
|
|
157
|
-
const ok = await bcrypt.compare(plain, hash);
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
## Quick checklist
|
|
161
|
-
|
|
162
|
-
- [ ] Put secrets in `.env` (don't commit)
|
|
163
|
-
- [ ] Verify auth on **every protected API** call
|
|
164
|
-
- [ ] Return `401` for missing/invalid, `403` for insufficient
|
|
165
|
-
- [ ] Use `HttpOnly` cookies for browser-only apps, or `Authorization` header for SPAs calling external
|
|
166
|
-
- [ ] Don't assume client-side redirects are enough
|
|
167
|
-
- [ ] Keep token expiry short, refresh if needed
|
|
168
|
-
|
|
169
|
-
## Continue
|
|
170
|
-
|
|
171
|
-
→ [Production & Deployment](./12-production-and-deployment.md)
|
|
1
|
+
# 11 · Authentication
|
|
2
|
+
|
|
3
|
+
There's no built-in auth. Because API routes run in Node and the browser client is flexible, you can add JWT-based, session-based (cookies), or OAuth flows yourself.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
- **Don't trust the browser.** Auth must be checked in **API routes** (server), not just hidden in UI.
|
|
8
|
+
- **Protect routes.** Return `401`/`403` from API handlers when unauthorized.
|
|
9
|
+
- **Send credentials in requests.** Use `Authorization: Bearer <token>` or rely on `HttpOnly` cookies.
|
|
10
|
+
- **Keep secrets in `.env`.** Never put `JWT_SECRET` in page/components.
|
|
11
|
+
|
|
12
|
+
## JWT (stateless) — minimal example
|
|
13
|
+
|
|
14
|
+
### 1. Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install jsonwebtoken
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### 2. `api/login.js`
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// api/login.js
|
|
24
|
+
const { NextResponse } = require('../_framework/next-response');
|
|
25
|
+
const jwt = require('jsonwebtoken');
|
|
26
|
+
|
|
27
|
+
const SECRET = process.env.JWT_SECRET || 'change-me';
|
|
28
|
+
|
|
29
|
+
module.exports = {
|
|
30
|
+
POST: async (request) => {
|
|
31
|
+
const { email, password } = await request.json();
|
|
32
|
+
if (!email || !password) return NextResponse.json({ error: 'Missing fields' }, { status: 400 });
|
|
33
|
+
|
|
34
|
+
// Fake user check for demo — replace with DB lookup + bcrypt
|
|
35
|
+
if (email === 'demo@example.com' && password === 'demo') {
|
|
36
|
+
const token = jwt.sign({ sub: 'u_123', email }, SECRET, { expiresIn: '7d' });
|
|
37
|
+
return NextResponse.json({ token });
|
|
38
|
+
}
|
|
39
|
+
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 3. `api/me.js` (protected)
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
// api/me.js
|
|
48
|
+
const { NextResponse } = require('../_framework/next-response');
|
|
49
|
+
const jwt = require('jsonwebtoken');
|
|
50
|
+
|
|
51
|
+
const SECRET = process.env.JWT_SECRET || 'change-me';
|
|
52
|
+
|
|
53
|
+
function getUser(req) {
|
|
54
|
+
const auth = req.headers.authorization || '';
|
|
55
|
+
if (!auth.startsWith('Bearer ')) return null;
|
|
56
|
+
const token = auth.slice(7);
|
|
57
|
+
try {
|
|
58
|
+
return jwt.verify(token, SECRET);
|
|
59
|
+
} catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
GET: async (request) => {
|
|
66
|
+
const user = getUser(request);
|
|
67
|
+
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
68
|
+
return NextResponse.json({ user });
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### 4. Browser usage
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
async function login() {
|
|
77
|
+
const res = await api.post('/api/login', { email: 'demo@example.com', password: 'demo' });
|
|
78
|
+
localStorage.setItem('token', res.token);
|
|
79
|
+
api.setHeaders({ Authorization: `Bearer ${res.token}` });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function me() {
|
|
83
|
+
const res = await api.get('/api/me');
|
|
84
|
+
console.log(res.user);
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
If you prefer not to keep in `localStorage`, have the login endpoint set an **HttpOnly + Secure + SameSite** cookie via `NextResponse.cookies.set` and send it on every request (the browser does this automatically). Then read `request.cookies` on the server.
|
|
89
|
+
|
|
90
|
+
## Sessions with cookies (HttpOnly)
|
|
91
|
+
|
|
92
|
+
```js
|
|
93
|
+
const sessions = new Map(); // in-memory; replace with Redis for multi-instance
|
|
94
|
+
|
|
95
|
+
module.exports.POST = async (request) => {
|
|
96
|
+
const { email, password } = await request.json();
|
|
97
|
+
if (email !== 'demo@example.com' || password !== 'demo') {
|
|
98
|
+
return NextResponse.json({ error: 'Invalid' }, { status: 401 });
|
|
99
|
+
}
|
|
100
|
+
const sid = crypto.randomUUID();
|
|
101
|
+
sessions.set(sid, { user: { id: 1, email } });
|
|
102
|
+
const res = NextResponse.json({ ok: true });
|
|
103
|
+
res.cookies.set('sid', sid, { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 86400 });
|
|
104
|
+
return res;
|
|
105
|
+
};
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Then check `request.cookies.sid` against `sessions`.
|
|
109
|
+
|
|
110
|
+
## Protecting pages (UI guard)
|
|
111
|
+
|
|
112
|
+
You can gate the UI in a page. The server remains the source of truth.
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
export default function Dashboard() {
|
|
116
|
+
const [ok, setOk] = useState(false);
|
|
117
|
+
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
api.get('/api/me')
|
|
120
|
+
.then(() => setOk(true))
|
|
121
|
+
.catch(() => navigate('/login'));
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
if (!ok) return <p>Checking auth…</p>;
|
|
125
|
+
return <h1>Dashboard</h1>;
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Protecting with roles
|
|
130
|
+
|
|
131
|
+
Attach a `role` to the token/session and check it:
|
|
132
|
+
|
|
133
|
+
```js
|
|
134
|
+
function requireRole(role) {
|
|
135
|
+
return async (request) => {
|
|
136
|
+
const user = getUser(request);
|
|
137
|
+
if (!user) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
138
|
+
if (user.role !== role) return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
|
|
139
|
+
return NextResponse.json({ ok: true, user });
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
module.exports.GET = requireRole('admin');
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Passwords
|
|
147
|
+
|
|
148
|
+
**Never** compare plain text. Use `bcrypt` or `argon2`:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npm install bcryptjs
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
const bcrypt = require('bcryptjs');
|
|
156
|
+
const hash = await bcrypt.hash(password, 10);
|
|
157
|
+
const ok = await bcrypt.compare(plain, hash);
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Quick checklist
|
|
161
|
+
|
|
162
|
+
- [ ] Put secrets in `.env` (don't commit)
|
|
163
|
+
- [ ] Verify auth on **every protected API** call
|
|
164
|
+
- [ ] Return `401` for missing/invalid, `403` for insufficient
|
|
165
|
+
- [ ] Use `HttpOnly` cookies for browser-only apps, or `Authorization` header for SPAs calling external
|
|
166
|
+
- [ ] Don't assume client-side redirects are enough
|
|
167
|
+
- [ ] Keep token expiry short, refresh if needed
|
|
168
|
+
|
|
169
|
+
## Continue
|
|
170
|
+
|
|
171
|
+
→ [Production & Deployment](./12-production-and-deployment.md)
|
|
@@ -1,121 +1,121 @@
|
|
|
1
|
-
# 12 · Production & Deployment
|
|
2
|
-
|
|
3
|
-
Aziz.js doesn't "bundle" to a huge build — it serves your source, compiles JSX on demand, and serves `/_modules/*` straight from `node_modules`. There's nothing to compile to a `dist/` directory.
|
|
4
|
-
|
|
5
|
-
## npm scripts
|
|
6
|
-
|
|
7
|
-
```json
|
|
8
|
-
{
|
|
9
|
-
"scripts": {
|
|
10
|
-
"dev": "node scripts/dev-server.js",
|
|
11
|
-
"build": "node scripts/dev-server.js --production",
|
|
12
|
-
"start": "node scripts/dev-server.js --production"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
```
|
|
16
|
-
|
|
17
|
-
**Running with `--production` (or `NODE_ENV=production`) changes behavior:**
|
|
18
|
-
|
|
19
|
-
- **No WebSocket live reload** server/client — the client script becomes an empty no-op.
|
|
20
|
-
- **Cache headers**: HTML/JSON are `no-cache` (check ETag), most static assets get long-lived cache with `ETag` still checked.
|
|
21
|
-
- **No verbose "Browser connected" / watcher spam** in the logs.
|
|
22
|
-
- **Routes manifest still served** same way.
|
|
23
|
-
|
|
24
|
-
## Locally testing production
|
|
25
|
-
|
|
26
|
-
```bash
|
|
27
|
-
npm run build # or npm start
|
|
28
|
-
# Server runs at http://localhost:3000 with production headers
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Visiting `/_framework/routes.json` returns `"dev": false` in production.
|
|
32
|
-
|
|
33
|
-
## Deploying to a host
|
|
34
|
-
|
|
35
|
-
Any host that can run **Node.js 18+** works. Examples: Render, Railway, Fly.io, Hetzner, DigitalOcean, Vercel (with a custom Node server), or a VPS.
|
|
36
|
-
|
|
37
|
-
### 1) Prepare the project
|
|
38
|
-
|
|
39
|
-
```bash
|
|
40
|
-
cd my-app
|
|
41
|
-
npm ci # clean install in CI/host
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
### 2) Environment variables
|
|
45
|
-
|
|
46
|
-
Set anything your API routes need: `MONGODB_URI`, `JWT_SECRET`, `PORT` etc. The server reads `process.env.PORT`.
|
|
47
|
-
|
|
48
|
-
### 3) Start it
|
|
49
|
-
|
|
50
|
-
On the host, run:
|
|
51
|
-
|
|
52
|
-
```bash
|
|
53
|
-
NODE_ENV=production node scripts/dev-server.js
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
Or `npm start` if you prefer.
|
|
57
|
-
|
|
58
|
-
If the platform requires a specific `start` command and binds to `$PORT`, this is fine — the server already respects `process.env.PORT`.
|
|
59
|
-
|
|
60
|
-
### 4) Proxy / reverse proxy (optional but recommended)
|
|
61
|
-
|
|
62
|
-
Put Nginx, Caddy, or your load balancer in front for:
|
|
63
|
-
|
|
64
|
-
- HTTPS termination
|
|
65
|
-
- Compression (gzip/brotli)
|
|
66
|
-
- Caching static assets aggressively
|
|
67
|
-
- Forwarding `X-Forwarded-For` / real IPs (the dev server doesn't trust proxy headers — treat as internal unless you add it)
|
|
68
|
-
|
|
69
|
-
Caddy example:
|
|
70
|
-
|
|
71
|
-
```txt
|
|
72
|
-
example.com {
|
|
73
|
-
reverse_proxy localhost:3000
|
|
74
|
-
encode zstd gzip
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Static hosting?
|
|
79
|
-
|
|
80
|
-
You **cannot** just drop `template/` into a static S3 bucket. API routes (`api/**` and `app/api/**/route.js`) run on the **Node server**. The browser needs the dev server to:
|
|
81
|
-
|
|
82
|
-
- serve transformed JS (JSX → h())
|
|
83
|
-
- serve the route manifest
|
|
84
|
-
- resolve bare imports via `/_modules/*`
|
|
85
|
-
- handle `/api/*`
|
|
86
|
-
|
|
87
|
-
If you want a 100% static site, you'd need a separate SSR/bundler — that's not what this lightweight framework does.
|
|
88
|
-
|
|
89
|
-
## Security notes
|
|
90
|
-
|
|
91
|
-
- **Path traversal**: the server confines all static file requests to the project root. Attempts to escape (e.g. `..%2f..%2fpackage.json`) return `403`.
|
|
92
|
-
- **API CORS**: preflight `OPTIONS` and responses include permissive CORS headers — tighten if you only serve from one origin.
|
|
93
|
-
- **JWT secrets**: never commit `.env`. Don't ship `node_modules` unnecessarily in git.
|
|
94
|
-
- **Dependencies**: only `@babel/standalone` in `devDependencies` (needed to transform JSX on-the-fly). Your own deps go in `dependencies`.
|
|
95
|
-
- **ETag**: conditional requests use strong-ish ETag based on `mtimeMs`+`size`; fine for this use case.
|
|
96
|
-
|
|
97
|
-
## What "production mode" doesn't do
|
|
98
|
-
|
|
99
|
-
- No code splitting / tree shaking
|
|
100
|
-
- No minification of JS/CSS (though you can pre-minify and reference extra files)
|
|
101
|
-
- No image optimization
|
|
102
|
-
- No prerendering/SSG
|
|
103
|
-
- No `.env` auto-loading (you must set environment variables)
|
|
104
|
-
|
|
105
|
-
This is by design for simplicity and speed. For most small–medium apps it's perfectly usable.
|
|
106
|
-
|
|
107
|
-
## Performance tips
|
|
108
|
-
|
|
109
|
-
- **Lean deps**: avoid pulling huge libraries unless needed (the `/_modules` transform isn't magic; it still ships bytes).
|
|
110
|
-
- **Memoize** in hooks (`useMemo`, `useCallback`) for expensive computations.
|
|
111
|
-
- **Keep keys stable** in lists (`key={id}`).
|
|
112
|
-
- **Use HTTP caching** via a proxy for immutable assets (`.woff2`, images) if you add them.
|
|
113
|
-
- **Close DB connections** or use a proper pool (the examples above do a singleton pool).
|
|
114
|
-
|
|
115
|
-
## Health check (optional)
|
|
116
|
-
|
|
117
|
-
You can hit `/api/hello` to quickly verify the server is up.
|
|
118
|
-
|
|
119
|
-
## Continue
|
|
120
|
-
|
|
121
|
-
→ [Internals](./13-internals.md)
|
|
1
|
+
# 12 · Production & Deployment
|
|
2
|
+
|
|
3
|
+
Aziz.js doesn't "bundle" to a huge build — it serves your source, compiles JSX on demand, and serves `/_modules/*` straight from `node_modules`. There's nothing to compile to a `dist/` directory.
|
|
4
|
+
|
|
5
|
+
## npm scripts
|
|
6
|
+
|
|
7
|
+
```json
|
|
8
|
+
{
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node scripts/dev-server.js",
|
|
11
|
+
"build": "node scripts/dev-server.js --production",
|
|
12
|
+
"start": "node scripts/dev-server.js --production"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Running with `--production` (or `NODE_ENV=production`) changes behavior:**
|
|
18
|
+
|
|
19
|
+
- **No WebSocket live reload** server/client — the client script becomes an empty no-op.
|
|
20
|
+
- **Cache headers**: HTML/JSON are `no-cache` (check ETag), most static assets get long-lived cache with `ETag` still checked.
|
|
21
|
+
- **No verbose "Browser connected" / watcher spam** in the logs.
|
|
22
|
+
- **Routes manifest still served** same way.
|
|
23
|
+
|
|
24
|
+
## Locally testing production
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm run build # or npm start
|
|
28
|
+
# Server runs at http://localhost:3000 with production headers
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Visiting `/_framework/routes.json` returns `"dev": false` in production.
|
|
32
|
+
|
|
33
|
+
## Deploying to a host
|
|
34
|
+
|
|
35
|
+
Any host that can run **Node.js 18+** works. Examples: Render, Railway, Fly.io, Hetzner, DigitalOcean, Vercel (with a custom Node server), or a VPS.
|
|
36
|
+
|
|
37
|
+
### 1) Prepare the project
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
cd my-app
|
|
41
|
+
npm ci # clean install in CI/host
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### 2) Environment variables
|
|
45
|
+
|
|
46
|
+
Set anything your API routes need: `MONGODB_URI`, `JWT_SECRET`, `PORT` etc. The server reads `process.env.PORT`.
|
|
47
|
+
|
|
48
|
+
### 3) Start it
|
|
49
|
+
|
|
50
|
+
On the host, run:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
NODE_ENV=production node scripts/dev-server.js
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or `npm start` if you prefer.
|
|
57
|
+
|
|
58
|
+
If the platform requires a specific `start` command and binds to `$PORT`, this is fine — the server already respects `process.env.PORT`.
|
|
59
|
+
|
|
60
|
+
### 4) Proxy / reverse proxy (optional but recommended)
|
|
61
|
+
|
|
62
|
+
Put Nginx, Caddy, or your load balancer in front for:
|
|
63
|
+
|
|
64
|
+
- HTTPS termination
|
|
65
|
+
- Compression (gzip/brotli)
|
|
66
|
+
- Caching static assets aggressively
|
|
67
|
+
- Forwarding `X-Forwarded-For` / real IPs (the dev server doesn't trust proxy headers — treat as internal unless you add it)
|
|
68
|
+
|
|
69
|
+
Caddy example:
|
|
70
|
+
|
|
71
|
+
```txt
|
|
72
|
+
example.com {
|
|
73
|
+
reverse_proxy localhost:3000
|
|
74
|
+
encode zstd gzip
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Static hosting?
|
|
79
|
+
|
|
80
|
+
You **cannot** just drop `template/` into a static S3 bucket. API routes (`api/**` and `app/api/**/route.js`) run on the **Node server**. The browser needs the dev server to:
|
|
81
|
+
|
|
82
|
+
- serve transformed JS (JSX → h())
|
|
83
|
+
- serve the route manifest
|
|
84
|
+
- resolve bare imports via `/_modules/*`
|
|
85
|
+
- handle `/api/*`
|
|
86
|
+
|
|
87
|
+
If you want a 100% static site, you'd need a separate SSR/bundler — that's not what this lightweight framework does.
|
|
88
|
+
|
|
89
|
+
## Security notes
|
|
90
|
+
|
|
91
|
+
- **Path traversal**: the server confines all static file requests to the project root. Attempts to escape (e.g. `..%2f..%2fpackage.json`) return `403`.
|
|
92
|
+
- **API CORS**: preflight `OPTIONS` and responses include permissive CORS headers — tighten if you only serve from one origin.
|
|
93
|
+
- **JWT secrets**: never commit `.env`. Don't ship `node_modules` unnecessarily in git.
|
|
94
|
+
- **Dependencies**: only `@babel/standalone` in `devDependencies` (needed to transform JSX on-the-fly). Your own deps go in `dependencies`.
|
|
95
|
+
- **ETag**: conditional requests use strong-ish ETag based on `mtimeMs`+`size`; fine for this use case.
|
|
96
|
+
|
|
97
|
+
## What "production mode" doesn't do
|
|
98
|
+
|
|
99
|
+
- No code splitting / tree shaking
|
|
100
|
+
- No minification of JS/CSS (though you can pre-minify and reference extra files)
|
|
101
|
+
- No image optimization
|
|
102
|
+
- No prerendering/SSG
|
|
103
|
+
- No `.env` auto-loading (you must set environment variables)
|
|
104
|
+
|
|
105
|
+
This is by design for simplicity and speed. For most small–medium apps it's perfectly usable.
|
|
106
|
+
|
|
107
|
+
## Performance tips
|
|
108
|
+
|
|
109
|
+
- **Lean deps**: avoid pulling huge libraries unless needed (the `/_modules` transform isn't magic; it still ships bytes).
|
|
110
|
+
- **Memoize** in hooks (`useMemo`, `useCallback`) for expensive computations.
|
|
111
|
+
- **Keep keys stable** in lists (`key={id}`).
|
|
112
|
+
- **Use HTTP caching** via a proxy for immutable assets (`.woff2`, images) if you add them.
|
|
113
|
+
- **Close DB connections** or use a proper pool (the examples above do a singleton pool).
|
|
114
|
+
|
|
115
|
+
## Health check (optional)
|
|
116
|
+
|
|
117
|
+
You can hit `/api/hello` to quickly verify the server is up.
|
|
118
|
+
|
|
119
|
+
## Continue
|
|
120
|
+
|
|
121
|
+
→ [Internals](./13-internals.md)
|