create-aziz-app 2.0.13 ā 2.0.15
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 +227 -0
- package/package.json +1 -1
- package/template/README.md +862 -138
- package/template/_framework/boot.js +36 -0
- package/template/_framework/hooks.js +207 -0
- package/template/_framework/router.js +279 -0
- package/template/_framework/runtime.js +86 -0
- package/template/api/createUser.js +69 -0
- package/template/api/getUser.js +60 -0
- package/template/api/getUserWithDB.js +65 -0
- package/template/api/hello.js +8 -17
- package/template/api/loginUser.js +61 -0
- package/template/api/users.js +38 -134
- package/template/app/about/page.js +45 -0
- package/template/app/layout.js +23 -0
- package/template/app/page.js +54 -0
- package/template/app/test/page.js +55 -0
- package/template/components/Footer.js +10 -0
- package/template/components/Navbar.js +30 -0
- package/template/docs/NEXTJS-APP-ROUTER.md +536 -0
- package/template/index.html +24 -0
- package/template/lib/db.js +67 -0
- package/template/models/User.js +102 -0
- package/template/modules/next-response.js +58 -0
- package/template/package.json +4 -10
- package/template/scripts/dev-server.js +357 -268
- package/template/scripts/live-reload-client.js +0 -1
|
@@ -1,369 +1,458 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const fs
|
|
3
|
+
const fs = require('fs');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const http = require('http');
|
|
6
|
-
const
|
|
6
|
+
const crypto = require('crypto');
|
|
7
7
|
|
|
8
8
|
const PORT = process.env.PORT || 3000;
|
|
9
|
+
const ROOT = process.cwd();
|
|
9
10
|
const clients = new Set();
|
|
10
11
|
const apiHandlers = {};
|
|
11
12
|
|
|
12
|
-
console.log('š
|
|
13
|
+
console.log('š AzizApp dev server starting...\n');
|
|
14
|
+
|
|
15
|
+
// āāā Route Manifest āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
16
|
+
// Scans app/ folder and builds a list of pages and layouts
|
|
17
|
+
|
|
18
|
+
function buildRouteManifest() {
|
|
19
|
+
const manifest = [];
|
|
20
|
+
const appDir = path.join(ROOT, 'app');
|
|
21
|
+
if (!fs.existsSync(appDir)) return manifest;
|
|
22
|
+
|
|
23
|
+
function scan(dir, routeBase) {
|
|
24
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
const fullPath = path.join(dir, entry.name);
|
|
28
|
+
|
|
29
|
+
if (entry.isDirectory()) {
|
|
30
|
+
// Dynamic route: [slug] ā :slug
|
|
31
|
+
const segment = entry.name.startsWith('[') && entry.name.endsWith(']')
|
|
32
|
+
? ':' + entry.name.slice(1, -1)
|
|
33
|
+
: entry.name;
|
|
34
|
+
scan(fullPath, routeBase === '/' ? '/' + segment : routeBase + '/' + segment);
|
|
35
|
+
} else if (entry.name === 'page.js' || entry.name === 'page.jsx') {
|
|
36
|
+
manifest.push({
|
|
37
|
+
type: 'page',
|
|
38
|
+
pathname: routeBase || '/',
|
|
39
|
+
filePath: '/app' + (routeBase === '/' ? '' : routeBase) + '/' + entry.name
|
|
40
|
+
});
|
|
41
|
+
} else if (entry.name === 'layout.js' || entry.name === 'layout.jsx') {
|
|
42
|
+
manifest.push({
|
|
43
|
+
type: 'layout',
|
|
44
|
+
pathname: routeBase || '/',
|
|
45
|
+
filePath: '/app' + (routeBase === '/' ? '' : routeBase) + '/' + entry.name
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
13
50
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
51
|
+
scan(appDir, '/');
|
|
52
|
+
return manifest;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// āāā Bare Import Rewriter āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
56
|
+
// Transforms: import X from 'mongoose' ā import X from '/_modules/mongoose'
|
|
57
|
+
// Transforms: import X from '@/lib/db' ā import X from '/lib/db.js'
|
|
58
|
+
|
|
59
|
+
function rewriteImports(code, filePath) {
|
|
60
|
+
// Rewrite @/ alias to absolute path
|
|
61
|
+
code = code.replace(
|
|
62
|
+
/from\s+['"]@\/([^'"]+)['"]/g,
|
|
63
|
+
(_, p) => `from '/${p}'`
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
// Rewrite bare specifiers (not starting with /, ./, ../)
|
|
67
|
+
code = code.replace(
|
|
68
|
+
/from\s+['"]([^'"./][^'"]*)['"]/g,
|
|
69
|
+
(match, specifier) => `from '/_modules/${specifier}'`
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
// Same for dynamic imports
|
|
73
|
+
code = code.replace(
|
|
74
|
+
/import\s*\(\s*['"]([^'"./][^'"]*)['"]\s*\)/g,
|
|
75
|
+
(_, specifier) => `import('/_modules/${specifier}')`
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return code;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// āāā Module Resolver āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
82
|
+
// Serves node_modules files for bare imports
|
|
83
|
+
|
|
84
|
+
function resolveNodeModule(specifier) {
|
|
85
|
+
// Try to find the package in node_modules
|
|
86
|
+
const parts = specifier.split('/');
|
|
87
|
+
const pkgName = parts[0].startsWith('@') ? parts[0] + '/' + parts[1] : parts[0];
|
|
88
|
+
const subPath = parts[0].startsWith('@') ? parts.slice(2).join('/') : parts.slice(1).join('/');
|
|
89
|
+
|
|
90
|
+
const nodeModulesDir = path.join(ROOT, 'node_modules');
|
|
91
|
+
|
|
92
|
+
// Try package.json to find main/module entry
|
|
93
|
+
const pkgJsonPath = path.join(nodeModulesDir, pkgName, 'package.json');
|
|
94
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
99
|
+
|
|
100
|
+
if (subPath) {
|
|
101
|
+
// Specific file requested
|
|
102
|
+
const candidates = [
|
|
103
|
+
path.join(nodeModulesDir, pkgName, subPath),
|
|
104
|
+
path.join(nodeModulesDir, pkgName, subPath + '.js'),
|
|
105
|
+
path.join(nodeModulesDir, pkgName, subPath + '/index.js'),
|
|
106
|
+
];
|
|
107
|
+
for (const c of candidates) {
|
|
108
|
+
if (fs.existsSync(c)) return c;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
20
111
|
}
|
|
21
112
|
|
|
113
|
+
// Use module > browser > main priority
|
|
114
|
+
const entry = pkg.module || pkg.browser || pkg.main || 'index.js';
|
|
115
|
+
const entryPath = path.join(nodeModulesDir, pkgName, entry);
|
|
116
|
+
return fs.existsSync(entryPath) ? entryPath : null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// āāā JSX Transform āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
120
|
+
// Adds /** @jsx h */ pragma if file uses JSX and doesn't have it
|
|
121
|
+
|
|
122
|
+
function ensureJsxPragma(code) {
|
|
123
|
+
if (code.includes('@jsx') || !code.includes('<')) return code;
|
|
124
|
+
// Simple heuristic: if it has JSX-like syntax, add pragma
|
|
125
|
+
if (/<[A-Z]|<[a-z]+[\s/>]/.test(code)) {
|
|
126
|
+
return '/** @jsx h */\n' + code;
|
|
127
|
+
}
|
|
128
|
+
return code;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// āāā API Route Loader āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
132
|
+
|
|
133
|
+
function loadApiRoutes() {
|
|
134
|
+
const apiDir = path.join(ROOT, 'api');
|
|
135
|
+
if (!fs.existsSync(apiDir)) return;
|
|
136
|
+
|
|
22
137
|
const files = fs.readdirSync(apiDir).filter(f => f.endsWith('.js'));
|
|
23
|
-
console.log(`š” Loading ${files.length} API
|
|
138
|
+
console.log(`š” Loading ${files.length} API route(s):`);
|
|
24
139
|
|
|
25
140
|
files.forEach(file => {
|
|
26
141
|
const routeName = path.basename(file, '.js');
|
|
27
|
-
const filePath
|
|
28
|
-
|
|
142
|
+
const filePath = path.join(apiDir, file);
|
|
143
|
+
|
|
29
144
|
try {
|
|
30
|
-
// Clear require cache to allow hot reload
|
|
31
145
|
delete require.cache[require.resolve(filePath)];
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (typeof handler === 'function') {
|
|
40
|
-
apiHandlers[routeName] = handler;
|
|
41
|
-
console.log(` ā
/api/${routeName}`);
|
|
146
|
+
const mod = require(filePath);
|
|
147
|
+
|
|
148
|
+
if (mod.GET || mod.POST || mod.PUT || mod.DELETE || mod.PATCH) {
|
|
149
|
+
const methods = {};
|
|
150
|
+
['GET','POST','PUT','DELETE','PATCH'].forEach(m => { if (mod[m]) methods[m] = mod[m]; });
|
|
151
|
+
apiHandlers[routeName] = methods;
|
|
152
|
+
console.log(` ā
/api/${routeName} [${Object.keys(methods).join(', ')}]`);
|
|
42
153
|
} else {
|
|
43
|
-
|
|
154
|
+
const handler = mod.default || mod;
|
|
155
|
+
if (typeof handler === 'function') {
|
|
156
|
+
apiHandlers[routeName] = handler;
|
|
157
|
+
console.log(` ā
/api/${routeName}`);
|
|
158
|
+
}
|
|
44
159
|
}
|
|
45
|
-
} catch (
|
|
46
|
-
console.error(` ā /api/${routeName}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(` ā /api/${routeName} ā ${err.message}`);
|
|
47
162
|
}
|
|
48
163
|
});
|
|
49
|
-
|
|
164
|
+
|
|
50
165
|
console.log('');
|
|
51
166
|
}
|
|
52
167
|
|
|
53
|
-
//
|
|
168
|
+
// āāā Request Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
169
|
+
|
|
54
170
|
function parseBody(req) {
|
|
55
171
|
return new Promise((resolve, reject) => {
|
|
56
172
|
let body = '';
|
|
57
|
-
req.on('data',
|
|
58
|
-
body += chunk.toString();
|
|
59
|
-
});
|
|
173
|
+
req.on('data', c => body += c);
|
|
60
174
|
req.on('end', () => {
|
|
61
175
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
resolve(body);
|
|
66
|
-
}
|
|
67
|
-
} catch (e) {
|
|
68
|
-
resolve(body);
|
|
69
|
-
}
|
|
176
|
+
resolve(body && req.headers['content-type']?.includes('application/json')
|
|
177
|
+
? JSON.parse(body) : body);
|
|
178
|
+
} catch { resolve(body); }
|
|
70
179
|
});
|
|
71
180
|
req.on('error', reject);
|
|
72
181
|
});
|
|
73
182
|
}
|
|
74
183
|
|
|
75
|
-
// Parse query string
|
|
76
184
|
function parseQuery(url) {
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
if (
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
return query;
|
|
185
|
+
const q = {};
|
|
186
|
+
const qs = url.split('?')[1];
|
|
187
|
+
if (qs) qs.split('&').forEach(p => {
|
|
188
|
+
const [k, v] = p.split('=');
|
|
189
|
+
q[decodeURIComponent(k)] = decodeURIComponent(v || '');
|
|
190
|
+
});
|
|
191
|
+
return q;
|
|
86
192
|
}
|
|
87
193
|
|
|
88
|
-
//
|
|
194
|
+
// āāā API Request Handler āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
195
|
+
|
|
89
196
|
async function handleApiRequest(req, res) {
|
|
90
|
-
const urlPath
|
|
197
|
+
const urlPath = req.url.split('?')[0];
|
|
91
198
|
const routeName = urlPath.replace('/api/', '');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
199
|
+
const handler = apiHandlers[routeName];
|
|
200
|
+
|
|
95
201
|
if (!handler) {
|
|
96
202
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
97
|
-
res.end(JSON.stringify({ error:
|
|
203
|
+
res.end(JSON.stringify({ error: `API route /api/${routeName} not found` }));
|
|
98
204
|
return;
|
|
99
205
|
}
|
|
100
206
|
|
|
101
207
|
try {
|
|
102
|
-
|
|
103
|
-
const body = await parseBody(req);
|
|
208
|
+
const body = await parseBody(req);
|
|
104
209
|
const query = parseQuery(req.url);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
method: req.method,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
});
|
|
128
|
-
res.end(JSON.stringify(
|
|
129
|
-
return
|
|
130
|
-
},
|
|
131
|
-
send(data) {
|
|
132
|
-
res.writeHead(this.statusCode, {
|
|
133
|
-
'Content-Type': 'text/plain',
|
|
134
|
-
'Access-Control-Allow-Origin': '*',
|
|
135
|
-
...this.headers
|
|
136
|
-
});
|
|
137
|
-
res.end(data);
|
|
138
|
-
return this;
|
|
139
|
-
},
|
|
140
|
-
setHeader(key, value) {
|
|
141
|
-
this.headers[key] = value;
|
|
142
|
-
return this;
|
|
210
|
+
|
|
211
|
+
if (typeof handler === 'function') {
|
|
212
|
+
// Old style
|
|
213
|
+
const apiReq = { method: req.method, body, query, headers: req.headers, url: req.url };
|
|
214
|
+
const apiRes = {
|
|
215
|
+
statusCode: 200, headers: {},
|
|
216
|
+
status(c) { this.statusCode = c; return this; },
|
|
217
|
+
json(d) {
|
|
218
|
+
res.writeHead(this.statusCode, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', ...this.headers });
|
|
219
|
+
res.end(JSON.stringify(d));
|
|
220
|
+
},
|
|
221
|
+
send(d) {
|
|
222
|
+
res.writeHead(this.statusCode, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*', ...this.headers });
|
|
223
|
+
res.end(d);
|
|
224
|
+
},
|
|
225
|
+
setHeader(k, v) { this.headers[k] = v; return this; }
|
|
226
|
+
};
|
|
227
|
+
await handler(apiReq, apiRes);
|
|
228
|
+
} else {
|
|
229
|
+
// New style: named exports
|
|
230
|
+
const methodFn = handler[req.method];
|
|
231
|
+
if (!methodFn) {
|
|
232
|
+
res.writeHead(405, { 'Content-Type': 'application/json', 'Allow': Object.keys(handler).join(', ') });
|
|
233
|
+
res.end(JSON.stringify({ error: 'Method not allowed', allowed: Object.keys(handler) }));
|
|
234
|
+
return;
|
|
143
235
|
}
|
|
144
|
-
};
|
|
145
236
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
}
|
|
237
|
+
const request = {
|
|
238
|
+
method: req.method,
|
|
239
|
+
headers: req.headers,
|
|
240
|
+
url: `http://localhost:${PORT}${req.url}`,
|
|
241
|
+
json: async () => body,
|
|
242
|
+
text: async () => typeof body === 'string' ? body : JSON.stringify(body)
|
|
243
|
+
};
|
|
155
244
|
|
|
156
|
-
|
|
157
|
-
function generateImports() {
|
|
158
|
-
console.log('š Regenerating imports...');
|
|
159
|
-
exec('node scripts/generate-imports.js', (error, stdout, stderr) => {
|
|
160
|
-
if (error) {
|
|
161
|
-
console.error('ā Error:', error);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
console.log(stdout);
|
|
165
|
-
// Notify all connected clients to reload
|
|
166
|
-
notifyClients();
|
|
167
|
-
});
|
|
168
|
-
}
|
|
245
|
+
const result = await methodFn(request);
|
|
169
246
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
try {
|
|
176
|
-
client.send('reload');
|
|
177
|
-
} catch (e) {
|
|
178
|
-
clients.delete(client);
|
|
247
|
+
if (result && result.body !== undefined) {
|
|
248
|
+
const headers = { 'Access-Control-Allow-Origin': '*', ...Object.fromEntries(result.headers || []) };
|
|
249
|
+
res.writeHead(result.status || 200, headers);
|
|
250
|
+
res.end(result.body);
|
|
251
|
+
}
|
|
179
252
|
}
|
|
180
|
-
})
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`ā API Error /api/${routeName}:`, err);
|
|
255
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
256
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
257
|
+
}
|
|
181
258
|
}
|
|
182
259
|
|
|
183
|
-
//
|
|
260
|
+
// āāā HTTP Server āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
261
|
+
|
|
262
|
+
const MIME = {
|
|
263
|
+
'.html': 'text/html',
|
|
264
|
+
'.js': 'text/javascript',
|
|
265
|
+
'.jsx': 'text/javascript',
|
|
266
|
+
'.mjs': 'text/javascript',
|
|
267
|
+
'.css': 'text/css',
|
|
268
|
+
'.json': 'application/json',
|
|
269
|
+
'.png': 'image/png',
|
|
270
|
+
'.jpg': 'image/jpeg',
|
|
271
|
+
'.svg': 'image/svg+xml',
|
|
272
|
+
'.ico': 'image/x-icon',
|
|
273
|
+
'.woff': 'font/woff',
|
|
274
|
+
'.woff2':'font/woff2',
|
|
275
|
+
'.ttf': 'font/ttf',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const NO_CACHE = {
|
|
279
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
280
|
+
'Pragma': 'no-cache',
|
|
281
|
+
'Expires': '0'
|
|
282
|
+
};
|
|
283
|
+
|
|
184
284
|
const server = http.createServer(async (req, res) => {
|
|
185
|
-
|
|
186
|
-
|
|
285
|
+
const urlWithoutQuery = req.url.split('?')[0];
|
|
286
|
+
|
|
287
|
+
// āā CORS preflight
|
|
288
|
+
if (req.method === 'OPTIONS') {
|
|
289
|
+
res.writeHead(200, { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type' });
|
|
290
|
+
res.end();
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// āā API routes
|
|
295
|
+
if (urlWithoutQuery.startsWith('/api/')) {
|
|
187
296
|
await handleApiRequest(req, res);
|
|
188
297
|
return;
|
|
189
298
|
}
|
|
190
299
|
|
|
191
|
-
//
|
|
192
|
-
if (
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
'Access-Control-Allow-Headers': 'Content-Type'
|
|
197
|
-
});
|
|
198
|
-
res.end();
|
|
300
|
+
// āā Route manifest (used by router.js to discover pages)
|
|
301
|
+
if (urlWithoutQuery === '/_framework/routes.json') {
|
|
302
|
+
const manifest = buildRouteManifest();
|
|
303
|
+
res.writeHead(200, { 'Content-Type': 'application/json', ...NO_CACHE });
|
|
304
|
+
res.end(JSON.stringify(manifest));
|
|
199
305
|
return;
|
|
200
306
|
}
|
|
201
307
|
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
filePath
|
|
308
|
+
// āā node_modules bare imports: /_modules/mongoose ā node_modules/mongoose
|
|
309
|
+
if (urlWithoutQuery.startsWith('/_modules/')) {
|
|
310
|
+
const specifier = urlWithoutQuery.slice('/_modules/'.length);
|
|
311
|
+
const filePath = resolveNodeModule(specifier);
|
|
312
|
+
|
|
313
|
+
if (!filePath) {
|
|
314
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
315
|
+
res.end(JSON.stringify({ error: `Module not found: ${specifier}. Run: npm install ${specifier.split('/')[0]}` }));
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
try {
|
|
320
|
+
let code = fs.readFileSync(filePath, 'utf8');
|
|
321
|
+
code = rewriteImports(code, filePath);
|
|
322
|
+
res.writeHead(200, { 'Content-Type': 'text/javascript', ...NO_CACHE });
|
|
323
|
+
res.end(code);
|
|
324
|
+
} catch (err) {
|
|
325
|
+
res.writeHead(500);
|
|
326
|
+
res.end('Error reading module: ' + err.message);
|
|
327
|
+
}
|
|
328
|
+
return;
|
|
206
329
|
}
|
|
207
330
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
'.html': 'text/html',
|
|
211
|
-
'.js': 'text/javascript',
|
|
212
|
-
'.jsx': 'text/javascript',
|
|
213
|
-
'.css': 'text/css',
|
|
214
|
-
'.json': 'application/json',
|
|
215
|
-
'.png': 'image/png',
|
|
216
|
-
'.jpg': 'image/jpg',
|
|
217
|
-
'.gif': 'image/gif',
|
|
218
|
-
'.svg': 'image/svg+xml',
|
|
219
|
-
'.wav': 'audio/wav',
|
|
220
|
-
'.mp4': 'video/mp4',
|
|
221
|
-
'.woff': 'application/font-woff',
|
|
222
|
-
'.ttf': 'application/font-ttf',
|
|
223
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
224
|
-
'.otf': 'application/font-otf',
|
|
225
|
-
'.wasm': 'application/wasm'
|
|
226
|
-
};
|
|
331
|
+
// āā Static files
|
|
332
|
+
let filePath = path.join(ROOT, urlWithoutQuery);
|
|
227
333
|
|
|
228
|
-
|
|
334
|
+
// Default to index.html
|
|
335
|
+
if (urlWithoutQuery === '/' || urlWithoutQuery === '') {
|
|
336
|
+
filePath = path.join(ROOT, 'index.html');
|
|
337
|
+
}
|
|
229
338
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
339
|
+
// SPA fallback: any path without extension ā index.html
|
|
340
|
+
const ext = path.extname(urlWithoutQuery);
|
|
341
|
+
if (!ext && urlWithoutQuery !== '/') {
|
|
342
|
+
filePath = path.join(ROOT, 'index.html');
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (!fs.existsSync(filePath)) {
|
|
346
|
+
// Try with .js extension
|
|
347
|
+
if (fs.existsSync(filePath + '.js')) {
|
|
348
|
+
filePath = filePath + '.js';
|
|
239
349
|
} else {
|
|
240
|
-
|
|
241
|
-
res.
|
|
242
|
-
|
|
243
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
244
|
-
'Pragma': 'no-cache',
|
|
245
|
-
'Expires': '0'
|
|
246
|
-
});
|
|
247
|
-
res.end(content, 'utf-8');
|
|
350
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
351
|
+
res.end('Not found: ' + urlWithoutQuery);
|
|
352
|
+
return;
|
|
248
353
|
}
|
|
249
|
-
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
try {
|
|
357
|
+
let content = fs.readFileSync(filePath);
|
|
358
|
+
const fileExt = path.extname(filePath).toLowerCase();
|
|
359
|
+
const contentType = MIME[fileExt] || 'application/octet-stream';
|
|
360
|
+
|
|
361
|
+
// Transform JS/JSX files: rewrite imports
|
|
362
|
+
if (fileExt === '.js' || fileExt === '.jsx' || fileExt === '.mjs') {
|
|
363
|
+
let code = content.toString();
|
|
364
|
+
code = rewriteImports(code, filePath);
|
|
365
|
+
content = Buffer.from(code);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
res.writeHead(200, { 'Content-Type': contentType, ...NO_CACHE });
|
|
369
|
+
res.end(content);
|
|
370
|
+
} catch (err) {
|
|
371
|
+
res.writeHead(500);
|
|
372
|
+
res.end('Server error: ' + err.message);
|
|
373
|
+
}
|
|
250
374
|
});
|
|
251
375
|
|
|
252
|
-
// WebSocket
|
|
253
|
-
|
|
254
|
-
|
|
376
|
+
// āāā WebSocket (live reload) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
377
|
+
|
|
378
|
+
server.on('upgrade', (req, socket) => {
|
|
255
379
|
const key = req.headers['sec-websocket-key'];
|
|
256
|
-
const
|
|
257
|
-
.createHash('sha1')
|
|
380
|
+
const accept = crypto.createHash('sha1')
|
|
258
381
|
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
259
382
|
.digest('base64');
|
|
260
383
|
|
|
261
384
|
socket.write(
|
|
262
385
|
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
263
|
-
'Upgrade: websocket\r\n' +
|
|
264
|
-
|
|
265
|
-
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
|
|
266
|
-
'\r\n'
|
|
386
|
+
'Upgrade: websocket\r\nConnection: Upgrade\r\n' +
|
|
387
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`
|
|
267
388
|
);
|
|
268
389
|
|
|
269
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
270
|
-
console.log(`š [${timestamp}] Browser connected (${clients.size + 1} total)`);
|
|
271
|
-
|
|
272
|
-
// Wrap socket with send method
|
|
273
390
|
const client = {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
frame
|
|
279
|
-
frame[1] = buffer.length;
|
|
280
|
-
buffer.copy(frame, 2);
|
|
391
|
+
send(msg) {
|
|
392
|
+
const buf = Buffer.from(msg);
|
|
393
|
+
const frame = Buffer.alloc(buf.length + 2);
|
|
394
|
+
frame[0] = 0x81; frame[1] = buf.length;
|
|
395
|
+
buf.copy(frame, 2);
|
|
281
396
|
socket.write(frame);
|
|
282
397
|
}
|
|
283
398
|
};
|
|
284
399
|
|
|
285
400
|
clients.add(client);
|
|
401
|
+
console.log(`š Browser connected (${clients.size} total)`);
|
|
286
402
|
|
|
287
|
-
socket.on('close', () => {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
403
|
+
socket.on('close', () => { clients.delete(client); });
|
|
404
|
+
socket.on('error', () => { clients.delete(client); });
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
function notifyClients() {
|
|
408
|
+
console.log(`š Reloading ${clients.size} browser(s)...`);
|
|
409
|
+
clients.forEach(c => { try { c.send('reload'); } catch { clients.delete(c); } });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// āāā File Watcher āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
292
413
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
414
|
+
const watchDirs = ['app', 'api', 'components', 'lib', 'models', 'styles', '_framework'];
|
|
415
|
+
let reloadTimer = null;
|
|
416
|
+
|
|
417
|
+
console.log('š Watching:');
|
|
418
|
+
watchDirs.forEach(dir => {
|
|
419
|
+
const full = path.join(ROOT, dir);
|
|
420
|
+
if (!fs.existsSync(full)) return;
|
|
421
|
+
console.log(` - ${dir}/`);
|
|
422
|
+
|
|
423
|
+
fs.watch(full, { recursive: true }, (_, filename) => {
|
|
424
|
+
if (!filename) return;
|
|
425
|
+
const ext = path.extname(filename);
|
|
426
|
+
if (!['.js', '.jsx', '.css', '.json'].includes(ext)) return;
|
|
427
|
+
|
|
428
|
+
console.log(`\nš Changed: ${dir}/${filename}`);
|
|
429
|
+
clearTimeout(reloadTimer);
|
|
430
|
+
|
|
431
|
+
if (dir === 'api') {
|
|
432
|
+
loadApiRoutes();
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
reloadTimer = setTimeout(notifyClients, 300);
|
|
296
436
|
});
|
|
297
437
|
});
|
|
298
438
|
|
|
299
|
-
//
|
|
439
|
+
// āāā Start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
440
|
+
|
|
300
441
|
function startServer(port) {
|
|
301
442
|
server.listen(port, () => {
|
|
302
|
-
console.log(
|
|
303
|
-
console.log(
|
|
304
|
-
|
|
443
|
+
console.log(`\nā
Dev server running!`);
|
|
444
|
+
console.log(`š http://localhost:${port}/\n`);
|
|
445
|
+
console.log('ā¹ļø Ctrl+C to stop\n');
|
|
446
|
+
}).on('error', err => {
|
|
305
447
|
if (err.code === 'EADDRINUSE') {
|
|
306
|
-
console.log(`ā ļø Port ${port}
|
|
448
|
+
console.log(`ā ļø Port ${port} busy, trying ${port + 1}...`);
|
|
307
449
|
startServer(port + 1);
|
|
308
450
|
} else {
|
|
309
|
-
console.error('ā
|
|
451
|
+
console.error('ā', err);
|
|
310
452
|
}
|
|
311
453
|
});
|
|
312
454
|
}
|
|
313
455
|
|
|
314
|
-
// Watch directories
|
|
315
|
-
const watchDirs = ['api', 'components', 'pages', 'modules', 'styles'];
|
|
316
|
-
|
|
317
|
-
// Debounce to prevent multiple rapid reloads
|
|
318
|
-
let reloadTimeout = null;
|
|
319
|
-
|
|
320
|
-
console.log('š Watching for changes in:');
|
|
321
|
-
watchDirs.forEach(dir => {
|
|
322
|
-
if (fs.existsSync(dir)) {
|
|
323
|
-
console.log(` - ${dir}/`);
|
|
324
|
-
fs.watch(dir, { recursive: false }, (eventType, filename) => {
|
|
325
|
-
if (filename && (filename.endsWith('.js') || filename.endsWith('.jsx') || filename.endsWith('.css'))) {
|
|
326
|
-
console.log(`\nš Detected change: ${dir}/${filename}`);
|
|
327
|
-
|
|
328
|
-
// Clear existing timeout
|
|
329
|
-
if (reloadTimeout) {
|
|
330
|
-
clearTimeout(reloadTimeout);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Reload API routes if api/ folder changed
|
|
334
|
-
if (dir === 'api' && filename.endsWith('.js')) {
|
|
335
|
-
console.log('š Reloading API routes...');
|
|
336
|
-
loadApiRoutes();
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
// For CSS changes, just reload without regenerating imports
|
|
340
|
-
if (filename.endsWith('.css')) {
|
|
341
|
-
reloadTimeout = setTimeout(() => {
|
|
342
|
-
notifyClients();
|
|
343
|
-
}, 300);
|
|
344
|
-
} else {
|
|
345
|
-
// For JS/JSX changes, regenerate imports
|
|
346
|
-
reloadTimeout = setTimeout(() => {
|
|
347
|
-
generateImports();
|
|
348
|
-
}, 500);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
});
|
|
352
|
-
}
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
console.log('\nā
Dev server running with live reload!');
|
|
356
|
-
console.log('š” Create/modify files and browser will auto-refresh');
|
|
357
|
-
console.log('ā¹ļø Press Ctrl+C to stop\n');
|
|
358
|
-
|
|
359
|
-
// Load API routes
|
|
360
456
|
loadApiRoutes();
|
|
361
|
-
|
|
362
|
-
// Start server
|
|
363
457
|
startServer(PORT);
|
|
364
|
-
|
|
365
|
-
// Generate imports on startup
|
|
366
|
-
generateImports();
|
|
367
|
-
|
|
368
|
-
// Keep the process running
|
|
369
458
|
process.stdin.resume();
|