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,381 +1,493 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
// ============================================================
|
|
4
|
-
// Aziz.js Dev Server
|
|
5
|
-
// - Serves the app with server-side JSX transform (cached)
|
|
6
|
-
// - API routes: api/** and app/api/**/route.js
|
|
7
|
-
// - node_modules bare-import resolution (/_modules/…)
|
|
8
|
-
// - WebSocket live reload (development only)
|
|
9
|
-
// - Production mode: node scripts/dev-server.js --production
|
|
10
|
-
// ============================================================
|
|
11
|
-
|
|
12
|
-
const fs = require('fs');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const http = require('http');
|
|
15
|
-
const crypto = require('crypto');
|
|
16
|
-
|
|
17
|
-
const { hasBabel, transformModule, invalidate: invalidateTransform } = require('./lib/js-transform');
|
|
18
|
-
const { resolveNodeModule } = require('./lib/module-resolver');
|
|
19
|
-
const apiRouter = require('./lib/api-router');
|
|
20
|
-
|
|
21
|
-
const PORT = parseInt(process.env.PORT, 10) || 3000;
|
|
22
|
-
const ROOT = process.cwd();
|
|
23
|
-
const PRODUCTION = process.argv.includes('--production') || process.env.NODE_ENV === 'production';
|
|
24
|
-
|
|
25
|
-
const clients = new Set();
|
|
26
|
-
let apiRoutes = [];
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
'
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
'
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Aziz.js Dev Server
|
|
5
|
+
// - Serves the app with server-side JSX transform (cached)
|
|
6
|
+
// - API routes: api/** and app/api/**/route.js
|
|
7
|
+
// - node_modules bare-import resolution (/_modules/…)
|
|
8
|
+
// - WebSocket live reload (development only)
|
|
9
|
+
// - Production mode: node scripts/dev-server.js --production
|
|
10
|
+
// ============================================================
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const http = require('http');
|
|
15
|
+
const crypto = require('crypto');
|
|
16
|
+
|
|
17
|
+
const { hasBabel, transformModule, invalidate: invalidateTransform } = require('./lib/js-transform');
|
|
18
|
+
const { resolveNodeModule } = require('./lib/module-resolver');
|
|
19
|
+
const apiRouter = require('./lib/api-router');
|
|
20
|
+
|
|
21
|
+
const PORT = parseInt(process.env.PORT, 10) || 3000;
|
|
22
|
+
const ROOT = process.cwd();
|
|
23
|
+
const PRODUCTION = process.argv.includes('--production') || process.env.NODE_ENV === 'production';
|
|
24
|
+
|
|
25
|
+
const clients = new Set();
|
|
26
|
+
let apiRoutes = [];
|
|
27
|
+
|
|
28
|
+
// ─── Files that must never reach the browser ───────────────────
|
|
29
|
+
//
|
|
30
|
+
// Aziz.js has no bundler, so every importable module is served straight
|
|
31
|
+
// from disk. That is what makes `import User from '@/models/User'` work —
|
|
32
|
+
// but it also means anything on disk is one HTTP request away, including
|
|
33
|
+
// .env secrets. These rules are the boundary between "code the browser
|
|
34
|
+
// may import" and "everything else".
|
|
35
|
+
|
|
36
|
+
const BLOCKED_FILES = new Set([
|
|
37
|
+
'package.json',
|
|
38
|
+
'package-lock.json',
|
|
39
|
+
'npm-shrinkwrap.json',
|
|
40
|
+
'yarn.lock',
|
|
41
|
+
'pnpm-lock.yaml',
|
|
42
|
+
'bun.lockb',
|
|
43
|
+
'tsconfig.json',
|
|
44
|
+
'jsconfig.json',
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
// Directories that only ever hold server-side code or data.
|
|
48
|
+
const BLOCKED_DIRS = new Set([
|
|
49
|
+
'node_modules', // reachable via /_modules/<pkg> instead
|
|
50
|
+
'server', // convention: server-only modules
|
|
51
|
+
'data', // convention: flat-file persistence (sessions, users)
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
// scripts/ holds the dev server itself, but index.html needs one file from it.
|
|
55
|
+
const ALLOWED_SCRIPT_FILES = new Set(['live-reload-client.js']);
|
|
56
|
+
|
|
57
|
+
// Explicit server-only modules: anything named *.server.{js,mjs,jsx}
|
|
58
|
+
const SERVER_MODULE_RE = /\.server\.(m?js|jsx)$/;
|
|
59
|
+
|
|
60
|
+
function forbiddenReason(urlPath) {
|
|
61
|
+
const parts = urlPath.split('/').filter(Boolean);
|
|
62
|
+
if (parts.length === 0) return null;
|
|
63
|
+
|
|
64
|
+
// Any dotfile or dot-directory: .env, .env.local, .git/config, .npmrc …
|
|
65
|
+
if (parts.some(p => p.startsWith('.'))) return 'dotfile';
|
|
66
|
+
|
|
67
|
+
const file = parts[parts.length - 1];
|
|
68
|
+
|
|
69
|
+
if (BLOCKED_FILES.has(file)) return 'project metadata file';
|
|
70
|
+
|
|
71
|
+
if (parts[0] === 'scripts') {
|
|
72
|
+
return ALLOWED_SCRIPT_FILES.has(file) ? null : 'dev-server internals';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (parts.some(p => BLOCKED_DIRS.has(p))) return 'server-only directory';
|
|
76
|
+
if (SERVER_MODULE_RE.test(file)) return 'server-only module';
|
|
77
|
+
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function box(lines) {
|
|
82
|
+
const width = Math.max(...lines.map(l => l.length)) + 2;
|
|
83
|
+
const bar = '─'.repeat(width + 2);
|
|
84
|
+
return [
|
|
85
|
+
'',
|
|
86
|
+
'┌' + bar + '┐',
|
|
87
|
+
...lines.map(l => '│ ' + l.padEnd(width) + ' │'),
|
|
88
|
+
'└' + bar + '┘',
|
|
89
|
+
''
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!hasBabel()) {
|
|
94
|
+
console.log(box([
|
|
95
|
+
'@babel/standalone is NOT installed — JSX cannot be compiled.',
|
|
96
|
+
'',
|
|
97
|
+
'Every page and layout will fail to load in the browser, so the',
|
|
98
|
+
'router falls back to its built-in 404 page. This looks like a',
|
|
99
|
+
'routing bug, but it is a missing dependency.',
|
|
100
|
+
'',
|
|
101
|
+
`Fix: ${fs.existsSync(path.join(ROOT, 'node_modules')) ? 'npm install --save-dev @babel/standalone' : 'npm install'}`,
|
|
102
|
+
` in ${ROOT}`,
|
|
103
|
+
'',
|
|
104
|
+
'Then restart the dev server.',
|
|
105
|
+
]));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`🚀 Aziz.js dev server starting${PRODUCTION ? ' (production mode)' : ''}...\n`);
|
|
109
|
+
|
|
110
|
+
// ─── Route Manifest ──────────────────────────────────────────
|
|
111
|
+
// Scans app/ and builds the page/layout/not-found/loading/error list for the
|
|
112
|
+
// client router.
|
|
113
|
+
|
|
114
|
+
const PAGE_NAMES = ['page.js', 'page.jsx'];
|
|
115
|
+
const LAYOUT_NAMES = ['layout.js', 'layout.jsx'];
|
|
116
|
+
const LOADING_NAMES = ['loading.js', 'loading.jsx'];
|
|
117
|
+
const ERROR_NAMES = ['error.js', 'error.jsx'];
|
|
118
|
+
// Next.js calls it not-found.js; 404.js is accepted as a friendlier alias.
|
|
119
|
+
const NOTFOUND_NAMES = ['not-found.js', 'not-found.jsx', '404.js', '404.jsx'];
|
|
120
|
+
|
|
121
|
+
function buildRouteManifest() {
|
|
122
|
+
const manifest = [];
|
|
123
|
+
const appDir = path.join(ROOT, 'app');
|
|
124
|
+
if (!fs.existsSync(appDir)) return manifest;
|
|
125
|
+
|
|
126
|
+
const filePathFor = (routeBase, fileName) =>
|
|
127
|
+
'/app' + (routeBase === '/' ? '' : routeBase) + '/' + fileName;
|
|
128
|
+
|
|
129
|
+
function scan(dir, routeBase) {
|
|
130
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
131
|
+
|
|
132
|
+
for (const entry of entries) {
|
|
133
|
+
const fullPath = path.join(dir, entry.name);
|
|
134
|
+
|
|
135
|
+
if (entry.isDirectory()) {
|
|
136
|
+
// Keep [slug] as-is — the router matcher expects bracket syntax
|
|
137
|
+
const segment = entry.name;
|
|
138
|
+
scan(fullPath, routeBase === '/' ? '/' + segment : routeBase + '/' + segment);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const name = entry.name;
|
|
143
|
+
|
|
144
|
+
if (PAGE_NAMES.includes(name)) {
|
|
145
|
+
manifest.push({ type: 'page', pathname: routeBase || '/', filePath: filePathFor(routeBase, name) });
|
|
146
|
+
} else if (LAYOUT_NAMES.includes(name)) {
|
|
147
|
+
manifest.push({ type: 'layout', pathname: routeBase || '/', filePath: filePathFor(routeBase, name) });
|
|
148
|
+
} else if (LOADING_NAMES.includes(name)) {
|
|
149
|
+
manifest.push({ type: 'loading', pathname: routeBase || '/', filePath: filePathFor(routeBase, name) });
|
|
150
|
+
} else if (ERROR_NAMES.includes(name)) {
|
|
151
|
+
manifest.push({ type: 'error', pathname: routeBase || '/', filePath: filePathFor(routeBase, name) });
|
|
152
|
+
} else if (NOTFOUND_NAMES.includes(name) && routeBase === '/') {
|
|
153
|
+
// filePath must use the real name — the old code hardcoded .js,
|
|
154
|
+
// which broke not-found.jsx.
|
|
155
|
+
manifest.push({ type: 'not-found', pathname: '/not-found', filePath: filePathFor(routeBase, name) });
|
|
156
|
+
}
|
|
157
|
+
// route.js files are API routes — handled by api-router, not the page router
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
scan(appDir, '/');
|
|
162
|
+
return manifest;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── API routes ──────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
async function reloadApiRoutes(verbose = true) {
|
|
168
|
+
apiRouter.purgeProjectRequireCache(ROOT);
|
|
169
|
+
const { routes } = await apiRouter.loadApiRoutes(ROOT, { verbose });
|
|
170
|
+
apiRoutes = routes;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// ─── HTTP Server ─────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
const MIME = {
|
|
176
|
+
'.html': 'text/html; charset=utf-8',
|
|
177
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
178
|
+
'.jsx': 'text/javascript; charset=utf-8',
|
|
179
|
+
'.mjs': 'text/javascript; charset=utf-8',
|
|
180
|
+
'.css': 'text/css; charset=utf-8',
|
|
181
|
+
'.json': 'application/json; charset=utf-8',
|
|
182
|
+
'.png': 'image/png',
|
|
183
|
+
'.jpg': 'image/jpeg',
|
|
184
|
+
'.jpeg': 'image/jpeg',
|
|
185
|
+
'.gif': 'image/gif',
|
|
186
|
+
'.webp': 'image/webp',
|
|
187
|
+
'.svg': 'image/svg+xml',
|
|
188
|
+
'.ico': 'image/x-icon',
|
|
189
|
+
'.woff': 'font/woff',
|
|
190
|
+
'.woff2':'font/woff2',
|
|
191
|
+
'.ttf': 'font/ttf',
|
|
192
|
+
'.map': 'application/json',
|
|
193
|
+
'.txt': 'text/plain; charset=utf-8',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const IMMUTABLE_EXTS = new Set(['.woff', '.woff2', '.ttf', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico']);
|
|
197
|
+
|
|
198
|
+
const NO_CACHE = {
|
|
199
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
200
|
+
'Pragma': 'no-cache',
|
|
201
|
+
'Expires': '0'
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
function cacheHeadersFor(fileExt, isHtml) {
|
|
205
|
+
if (!PRODUCTION) return NO_CACHE;
|
|
206
|
+
if (isHtml || fileExt === '.json') {
|
|
207
|
+
return { 'Cache-Control': 'no-cache, must-revalidate' };
|
|
208
|
+
}
|
|
209
|
+
if (IMMUTABLE_EXTS.has(fileExt)) {
|
|
210
|
+
return { 'Cache-Control': 'public, max-age=86400, immutable' };
|
|
211
|
+
}
|
|
212
|
+
return { 'Cache-Control': 'no-cache, must-revalidate' };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function etagFor(stat) {
|
|
216
|
+
return `"${stat.mtimeMs.toString(36)}-${stat.size.toString(36)}"`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const server = http.createServer(async (req, res) => {
|
|
220
|
+
const rawUrl = req.url;
|
|
221
|
+
const urlWithoutQuery = rawUrl.split('?')[0];
|
|
222
|
+
|
|
223
|
+
// ── API routes (api/** and app/api/**/route.js)
|
|
224
|
+
if (urlWithoutQuery === '/api' || urlWithoutQuery.startsWith('/api/')) {
|
|
225
|
+
await apiRouter.handleApiRequest(req, res, { routes: apiRoutes, port: server.address()?.port || PORT });
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ── Route manifest (used by the client router to discover pages)
|
|
230
|
+
if (urlWithoutQuery === '/_framework/routes.json') {
|
|
231
|
+
const manifest = buildRouteManifest();
|
|
232
|
+
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8', ...NO_CACHE });
|
|
233
|
+
res.end(JSON.stringify({ dev: !PRODUCTION, routes: manifest }));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ── In production, neutralize the live-reload client (no WS server)
|
|
238
|
+
if (PRODUCTION && urlWithoutQuery === '/scripts/live-reload-client.js') {
|
|
239
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript; charset=utf-8', 'Cache-Control': 'public, max-age=86400' });
|
|
240
|
+
res.end('/* live reload disabled in production */\n');
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── node_modules bare imports: /_modules/mongoose → node_modules/mongoose
|
|
245
|
+
if (urlWithoutQuery.startsWith('/_modules/')) {
|
|
246
|
+
let specifier;
|
|
247
|
+
try {
|
|
248
|
+
specifier = decodeURIComponent(urlWithoutQuery.slice('/_modules/'.length));
|
|
249
|
+
} catch {
|
|
250
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
251
|
+
res.end(JSON.stringify({ error: 'Malformed module specifier' }));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const filePath = resolveNodeModule(specifier, ROOT);
|
|
256
|
+
|
|
257
|
+
if (!filePath) {
|
|
258
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
259
|
+
res.end(JSON.stringify({ error: `Module not found: ${specifier}. Run: npm install ${specifier.split('/')[0]}` }));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
try {
|
|
264
|
+
const stat = fs.statSync(filePath);
|
|
265
|
+
let code = fs.readFileSync(filePath, 'utf8');
|
|
266
|
+
code = transformModule(code, filePath, stat.mtimeMs);
|
|
267
|
+
res.writeHead(200, {
|
|
268
|
+
'Content-Type': 'text/javascript; charset=utf-8',
|
|
269
|
+
...cacheHeadersFor('.js', false),
|
|
270
|
+
'ETag': etagFor(stat),
|
|
271
|
+
});
|
|
272
|
+
res.end(code);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
res.writeHead(500);
|
|
275
|
+
res.end('Error reading module: ' + err.message);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// ── Static files + SPA fallback
|
|
281
|
+
let filePath;
|
|
282
|
+
|
|
283
|
+
// Refuse secrets and server internals before touching the filesystem.
|
|
284
|
+
const blocked = forbiddenReason(urlWithoutQuery);
|
|
285
|
+
if (blocked) {
|
|
286
|
+
res.writeHead(404, { 'Content-Type': 'application/json; charset=utf-8', ...NO_CACHE });
|
|
287
|
+
res.end(JSON.stringify({
|
|
288
|
+
error: 'Not found',
|
|
289
|
+
detail: `'${urlWithoutQuery}' is not served by Aziz.js (${blocked}).`,
|
|
290
|
+
hint: 'Server-only code belongs in api/, server/, or a *.server.mjs file — import it from an API route, never from a page.',
|
|
291
|
+
}));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
if (urlWithoutQuery === '/' || urlWithoutQuery === '') {
|
|
297
|
+
filePath = path.join(ROOT, 'index.html');
|
|
298
|
+
} else {
|
|
299
|
+
let decoded;
|
|
300
|
+
try {
|
|
301
|
+
decoded = decodeURIComponent(urlWithoutQuery);
|
|
302
|
+
} catch {
|
|
303
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
304
|
+
res.end('Bad request');
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Security: resolve and CONFINE to the project root (blocks path traversal)
|
|
309
|
+
const resolved = path.resolve(ROOT, '.' + (decoded.startsWith('/') ? decoded : '/' + decoded));
|
|
310
|
+
const rootResolved = path.resolve(ROOT);
|
|
311
|
+
if (resolved !== rootResolved && !resolved.startsWith(rootResolved + path.sep)) {
|
|
312
|
+
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
313
|
+
res.end('Forbidden');
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
filePath = resolved;
|
|
317
|
+
|
|
318
|
+
// SPA fallback: path with no file extension → serve index.html
|
|
319
|
+
// so the client-side router handles it
|
|
320
|
+
const ext = path.extname(filePath);
|
|
321
|
+
if (!ext && !fs.existsSync(filePath)) {
|
|
322
|
+
filePath = path.join(ROOT, 'index.html');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
res.writeHead(400, { 'Content-Type': 'text/plain' });
|
|
327
|
+
res.end('Bad request');
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!fs.existsSync(filePath)) {
|
|
332
|
+
// Try appending .js / .jsx (import specifiers without extension)
|
|
333
|
+
if (fs.existsSync(filePath + '.js')) {
|
|
334
|
+
filePath = filePath + '.js';
|
|
335
|
+
} else if (fs.existsSync(filePath + '.jsx')) {
|
|
336
|
+
filePath = filePath + '.jsx';
|
|
337
|
+
} else if (path.extname(filePath) === '' && fs.existsSync(path.join(ROOT, 'index.html'))) {
|
|
338
|
+
filePath = path.join(ROOT, 'index.html');
|
|
339
|
+
} else {
|
|
340
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
341
|
+
res.end('Not found: ' + urlWithoutQuery);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
try {
|
|
347
|
+
const stat = fs.statSync(filePath);
|
|
348
|
+
const fileExt = path.extname(filePath).toLowerCase();
|
|
349
|
+
const isHtml = fileExt === '.html';
|
|
350
|
+
const contentType = MIME[fileExt] || 'application/octet-stream';
|
|
351
|
+
const headers = {
|
|
352
|
+
'Content-Type': contentType,
|
|
353
|
+
...cacheHeadersFor(fileExt, isHtml),
|
|
354
|
+
'ETag': etagFor(stat),
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
// Conditional request → 304
|
|
358
|
+
if (req.headers['if-none-match'] === headers.ETag) {
|
|
359
|
+
res.writeHead(304, headers);
|
|
360
|
+
res.end();
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let content = fs.readFileSync(filePath);
|
|
365
|
+
|
|
366
|
+
// Transform JS/JSX: JSX → h() + rewrite imports (mtime-cached)
|
|
367
|
+
if (fileExt === '.js' || fileExt === '.jsx' || fileExt === '.mjs') {
|
|
368
|
+
const code = transformModule(content.toString(), filePath, stat.mtimeMs);
|
|
369
|
+
content = Buffer.from(code);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
res.writeHead(200, headers);
|
|
373
|
+
res.end(content);
|
|
374
|
+
} catch (err) {
|
|
375
|
+
res.writeHead(500);
|
|
376
|
+
res.end('Server error: ' + err.message);
|
|
377
|
+
}
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
// ─── WebSocket (live reload, development only) ───────────────
|
|
381
|
+
|
|
382
|
+
if (!PRODUCTION) {
|
|
383
|
+
server.on('upgrade', (req, socket) => {
|
|
384
|
+
const key = req.headers['sec-websocket-key'];
|
|
385
|
+
if (!key) { socket.destroy(); return; }
|
|
386
|
+
const accept = crypto.createHash('sha1')
|
|
387
|
+
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
388
|
+
.digest('base64');
|
|
389
|
+
|
|
390
|
+
socket.write(
|
|
391
|
+
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
392
|
+
'Upgrade: websocket\r\nConnection: Upgrade\r\n' +
|
|
393
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
const client = {
|
|
397
|
+
send(msg) {
|
|
398
|
+
const buf = Buffer.from(msg);
|
|
399
|
+
if (buf.length > 125) return; // control messages are tiny
|
|
400
|
+
const frame = Buffer.alloc(buf.length + 2);
|
|
401
|
+
frame[0] = 0x81;
|
|
402
|
+
frame[1] = buf.length;
|
|
403
|
+
buf.copy(frame, 2);
|
|
404
|
+
socket.write(frame);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
clients.add(client);
|
|
409
|
+
console.log(`🔌 Browser connected (${clients.size} total)`);
|
|
410
|
+
|
|
411
|
+
socket.on('close', () => { clients.delete(client); });
|
|
412
|
+
socket.on('error', () => { clients.delete(client); });
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function notifyClients() {
|
|
417
|
+
if (clients.size === 0) return;
|
|
418
|
+
console.log(`🔄 Reloading ${clients.size} browser(s)...`);
|
|
419
|
+
clients.forEach(c => { try { c.send('reload'); } catch { clients.delete(c); } });
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// ─── File Watcher (development only) ─────────────────────────
|
|
423
|
+
//
|
|
424
|
+
// NOTE: on Windows, Node's directory watcher can abort the process with
|
|
425
|
+
// "Assertion failed: !_wcsnicmp(filename, dir, dirlen)" (src\win\fs-event.c).
|
|
426
|
+
// It is a native assertion that try/catch cannot intercept, it is unrelated
|
|
427
|
+
// to recursive:true (a non-recursive watcher hits it too), and it only shows
|
|
428
|
+
// up on trees with nested subdirectories — roughly 1 start in 8 on the
|
|
429
|
+
// machine this was observed on. It is reproducible enough to be worth
|
|
430
|
+
// reporting upstream, and rare enough that switching watcher strategies
|
|
431
|
+
// costs more than it saves. Restarting the dev server always clears it.
|
|
432
|
+
|
|
433
|
+
const watchDirs = ['app', 'api', 'components', 'lib', 'models', 'styles', '_framework'];
|
|
434
|
+
const SERVER_CODE_DIRS = new Set(['api', 'lib', 'models']);
|
|
435
|
+
let reloadTimer = null;
|
|
436
|
+
|
|
437
|
+
if (!PRODUCTION) {
|
|
438
|
+
console.log('👀 Watching:');
|
|
439
|
+
watchDirs.forEach(dir => {
|
|
440
|
+
const full = path.join(ROOT, dir);
|
|
441
|
+
if (!fs.existsSync(full)) return;
|
|
442
|
+
console.log(` - ${dir}/`);
|
|
443
|
+
|
|
444
|
+
try {
|
|
445
|
+
fs.watch(full, { recursive: true }, (_, filename) => {
|
|
446
|
+
if (!filename) return;
|
|
447
|
+
const ext = path.extname(filename);
|
|
448
|
+
if (!['.js', '.jsx', '.mjs', '.css', '.json'].includes(ext)) return;
|
|
449
|
+
|
|
450
|
+
console.log(`\n📝 Changed: ${dir}/${filename}`);
|
|
451
|
+
|
|
452
|
+
// Keep the transform cache fresh
|
|
453
|
+
invalidateTransform(path.join(full, filename));
|
|
454
|
+
|
|
455
|
+
if (SERVER_CODE_DIRS.has(dir)) {
|
|
456
|
+
// API routes may import from lib/ and models/ — reload them
|
|
457
|
+
clearTimeout(reloadTimer);
|
|
458
|
+
reloadTimer = setTimeout(() => {
|
|
459
|
+
reloadApiRoutes(false).then(notifyClients);
|
|
460
|
+
}, 200);
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
clearTimeout(reloadTimer);
|
|
465
|
+
reloadTimer = setTimeout(notifyClients, 300);
|
|
466
|
+
});
|
|
467
|
+
} catch (err) {
|
|
468
|
+
console.warn(` ⚠️ Could not watch ${dir}/: ${err.message}`);
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// ─── Start ────────────────────────────────────────────────────
|
|
474
|
+
|
|
475
|
+
function startServer(port) {
|
|
476
|
+
server.listen(port, () => {
|
|
477
|
+
console.log(`\n✅ Server running!`);
|
|
478
|
+
console.log(`🌐 http://localhost:${port}/`);
|
|
479
|
+
console.log(PRODUCTION ? '⏹️ Ctrl+C to stop\n' : '⏹️ Ctrl+C to stop\n');
|
|
480
|
+
}).on('error', err => {
|
|
481
|
+
if (err.code === 'EADDRINUSE') {
|
|
482
|
+
console.log(`⚠️ Port ${port} busy, trying ${port + 1}...`);
|
|
483
|
+
startServer(port + 1);
|
|
484
|
+
} else {
|
|
485
|
+
console.error('❌', err);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
reloadApiRoutes().then(() => startServer(PORT)).catch(err => {
|
|
491
|
+
console.error('❌ Failed to load API routes:', err);
|
|
492
|
+
startServer(PORT);
|
|
493
|
+
});
|