create-aziz-app 2.0.14 ā 3.0.0
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/bin/create-aziz-app.js +9 -9
- package/package.json +10 -11
- package/template/README.md +862 -138
- package/template/_framework/boot.js +36 -0
- package/template/_framework/hooks.js +207 -0
- package/template/{modules ā _framework}/next-response.js +5 -13
- package/template/_framework/router.js +279 -0
- package/template/_framework/runtime.js +86 -0
- package/template/api/createUser.js +1 -1
- package/template/api/hello.js +8 -17
- package/template/api/loginUser.js +1 -1
- 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/index.html +24 -0
- package/template/package.json +5 -14
- package/template/scripts/dev-server.js +330 -302
- package/template/scripts/live-reload-client.js +0 -1
- package/template/.vscode/settings.json +0 -15
- package/template/api/getUser.js +0 -60
- package/template/api/getUserWithDB.js +0 -65
- package/template/app.config.js +0 -41
- package/template/app.html +0 -55
- package/template/components/Counter.jsx +0 -36
- package/template/components/Footer.jsx +0 -9
- package/template/components/Navbar.jsx +0 -17
- package/template/docs/API-GUIDE.md +0 -196
- package/template/docs/API-ROUTES.md +0 -538
- package/template/docs/AUTO-IMPORT.md +0 -99
- package/template/docs/AUTO-REGISTRATION.md +0 -248
- package/template/docs/CSS-FRAMEWORKS.md +0 -221
- package/template/docs/DEV-SERVER.md +0 -184
- package/template/docs/GETTING-STARTED.md +0 -205
- package/template/docs/HOW-IT-WORKS.md +0 -362
- package/template/docs/NEXTJS-APP-ROUTER.md +0 -536
- package/template/docs/NEXTJS-COMPARISON.md +0 -473
- package/template/docs/QUICK-START.md +0 -111
- package/template/docs/STYLING.md +0 -269
- package/template/docs/TROUBLESHOOTING.md +0 -282
- package/template/modules/README.md +0 -129
- package/template/modules/api.js +0 -44
- package/template/modules/auto-import.js +0 -130
- package/template/modules/router.js +0 -218
- package/template/modules/state.js +0 -248
- package/template/package-lock.json +0 -13
- package/template/pages/About.jsx +0 -48
- package/template/pages/ApiDemo.jsx +0 -209
- package/template/pages/Contact.jsx +0 -25
- package/template/pages/Example.jsx +0 -59
- package/template/pages/Home.jsx +0 -39
- package/template/pages/HooksDemo.jsx +0 -194
- package/template/pages/test.jsx +0 -72
- package/template/scripts/build.js +0 -81
- package/template/scripts/generate-imports.js +0 -88
- package/template/tailwind.config.js +0 -17
|
@@ -1,188 +1,239 @@
|
|
|
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('š Aziz.js 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
|
-
// Support both styles:
|
|
37
|
-
// 1. Default export function (old style)
|
|
38
|
-
// 2. Named exports: GET, POST, PUT, DELETE (new Next.js App Router style)
|
|
39
|
-
|
|
40
|
-
if (module.GET || module.POST || module.PUT || module.DELETE || module.PATCH) {
|
|
41
|
-
// New style: Named exports
|
|
146
|
+
const mod = require(filePath);
|
|
147
|
+
|
|
148
|
+
if (mod.GET || mod.POST || mod.PUT || mod.DELETE || mod.PATCH) {
|
|
42
149
|
const methods = {};
|
|
43
|
-
if (
|
|
44
|
-
if (module.POST) methods.POST = module.POST;
|
|
45
|
-
if (module.PUT) methods.PUT = module.PUT;
|
|
46
|
-
if (module.DELETE) methods.DELETE = module.DELETE;
|
|
47
|
-
if (module.PATCH) methods.PATCH = module.PATCH;
|
|
48
|
-
|
|
150
|
+
['GET','POST','PUT','DELETE','PATCH'].forEach(m => { if (mod[m]) methods[m] = mod[m]; });
|
|
49
151
|
apiHandlers[routeName] = methods;
|
|
50
152
|
console.log(` ā
/api/${routeName} [${Object.keys(methods).join(', ')}]`);
|
|
51
153
|
} else {
|
|
52
|
-
|
|
53
|
-
const handler = module.default || module;
|
|
54
|
-
|
|
154
|
+
const handler = mod.default || mod;
|
|
55
155
|
if (typeof handler === 'function') {
|
|
56
156
|
apiHandlers[routeName] = handler;
|
|
57
157
|
console.log(` ā
/api/${routeName}`);
|
|
58
|
-
} else {
|
|
59
|
-
console.log(` ā ļø /api/${routeName} - No handler function exported`);
|
|
60
158
|
}
|
|
61
159
|
}
|
|
62
|
-
} catch (
|
|
63
|
-
console.error(` ā /api/${routeName}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error(` ā /api/${routeName} ā ${err.message}`);
|
|
64
162
|
}
|
|
65
163
|
});
|
|
66
|
-
|
|
164
|
+
|
|
67
165
|
console.log('');
|
|
68
166
|
}
|
|
69
|
-
}
|
|
70
167
|
|
|
71
|
-
//
|
|
168
|
+
// āāā Request Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
169
|
+
|
|
72
170
|
function parseBody(req) {
|
|
73
171
|
return new Promise((resolve, reject) => {
|
|
74
172
|
let body = '';
|
|
75
|
-
req.on('data',
|
|
76
|
-
body += chunk.toString();
|
|
77
|
-
});
|
|
173
|
+
req.on('data', c => body += c);
|
|
78
174
|
req.on('end', () => {
|
|
79
175
|
try {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
resolve(body);
|
|
84
|
-
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
resolve(body);
|
|
87
|
-
}
|
|
176
|
+
resolve(body && req.headers['content-type']?.includes('application/json')
|
|
177
|
+
? JSON.parse(body) : body);
|
|
178
|
+
} catch { resolve(body); }
|
|
88
179
|
});
|
|
89
180
|
req.on('error', reject);
|
|
90
181
|
});
|
|
91
182
|
}
|
|
92
183
|
|
|
93
|
-
// Parse query string
|
|
94
184
|
function parseQuery(url) {
|
|
95
|
-
const
|
|
96
|
-
const
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
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;
|
|
104
192
|
}
|
|
105
193
|
|
|
106
|
-
//
|
|
194
|
+
// āāā API Request Handler āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
195
|
+
|
|
107
196
|
async function handleApiRequest(req, res) {
|
|
108
|
-
const urlPath
|
|
197
|
+
const urlPath = req.url.split('?')[0];
|
|
109
198
|
const routeName = urlPath.replace('/api/', '');
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
199
|
+
const handler = apiHandlers[routeName];
|
|
200
|
+
|
|
113
201
|
if (!handler) {
|
|
114
202
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
115
|
-
res.end(JSON.stringify({ error:
|
|
203
|
+
res.end(JSON.stringify({ error: `API route /api/${routeName} not found` }));
|
|
116
204
|
return;
|
|
117
205
|
}
|
|
118
206
|
|
|
119
207
|
try {
|
|
120
|
-
|
|
121
|
-
const body = await parseBody(req);
|
|
208
|
+
const body = await parseBody(req);
|
|
122
209
|
const query = parseQuery(req.url);
|
|
123
|
-
|
|
124
|
-
// Check if handler is a function (old style) or object with HTTP methods (new style)
|
|
125
|
-
let result;
|
|
126
|
-
|
|
127
|
-
if (typeof handler === 'function') {
|
|
128
|
-
// Old style: default export function
|
|
129
|
-
const apiReq = {
|
|
130
|
-
method: req.method,
|
|
131
|
-
body: body,
|
|
132
|
-
query: query,
|
|
133
|
-
headers: req.headers,
|
|
134
|
-
url: req.url
|
|
135
|
-
};
|
|
136
210
|
|
|
211
|
+
if (typeof handler === 'function') {
|
|
212
|
+
// Old style
|
|
213
|
+
const apiReq = { method: req.method, body, query, headers: req.headers, url: req.url };
|
|
137
214
|
const apiRes = {
|
|
138
|
-
statusCode: 200,
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.statusCode
|
|
142
|
-
|
|
143
|
-
},
|
|
144
|
-
json(data) {
|
|
145
|
-
res.writeHead(this.statusCode, {
|
|
146
|
-
'Content-Type': 'application/json',
|
|
147
|
-
'Access-Control-Allow-Origin': '*',
|
|
148
|
-
...this.headers
|
|
149
|
-
});
|
|
150
|
-
res.end(JSON.stringify(data));
|
|
151
|
-
return this;
|
|
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));
|
|
152
220
|
},
|
|
153
|
-
send(
|
|
154
|
-
res.writeHead(this.statusCode, {
|
|
155
|
-
|
|
156
|
-
'Access-Control-Allow-Origin': '*',
|
|
157
|
-
...this.headers
|
|
158
|
-
});
|
|
159
|
-
res.end(data);
|
|
160
|
-
return this;
|
|
221
|
+
send(d) {
|
|
222
|
+
res.writeHead(this.statusCode, { 'Content-Type': 'text/plain', 'Access-Control-Allow-Origin': '*', ...this.headers });
|
|
223
|
+
res.end(d);
|
|
161
224
|
},
|
|
162
|
-
setHeader(
|
|
163
|
-
this.headers[key] = value;
|
|
164
|
-
return this;
|
|
165
|
-
}
|
|
225
|
+
setHeader(k, v) { this.headers[k] = v; return this; }
|
|
166
226
|
};
|
|
167
|
-
|
|
168
227
|
await handler(apiReq, apiRes);
|
|
169
|
-
} else
|
|
170
|
-
// New style:
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
res.
|
|
175
|
-
'Content-Type': 'application/json',
|
|
176
|
-
'Allow': Object.keys(handler).join(', ')
|
|
177
|
-
});
|
|
178
|
-
res.end(JSON.stringify({
|
|
179
|
-
error: 'Method not allowed',
|
|
180
|
-
allowedMethods: Object.keys(handler)
|
|
181
|
-
}));
|
|
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) }));
|
|
182
234
|
return;
|
|
183
235
|
}
|
|
184
236
|
|
|
185
|
-
// Create Next.js-style request object
|
|
186
237
|
const request = {
|
|
187
238
|
method: req.method,
|
|
188
239
|
headers: req.headers,
|
|
@@ -191,240 +242,217 @@ async function handleApiRequest(req, res) {
|
|
|
191
242
|
text: async () => typeof body === 'string' ? body : JSON.stringify(body)
|
|
192
243
|
};
|
|
193
244
|
|
|
194
|
-
|
|
195
|
-
result = await methodHandler(request);
|
|
245
|
+
const result = await methodFn(request);
|
|
196
246
|
|
|
197
|
-
// Handle NextResponse
|
|
198
247
|
if (result && result.body !== undefined) {
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
'Access-Control-Allow-Origin': '*',
|
|
202
|
-
...Object.fromEntries(result.headers || [])
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
res.writeHead(statusCode, headers);
|
|
248
|
+
const headers = { 'Access-Control-Allow-Origin': '*', ...Object.fromEntries(result.headers || []) };
|
|
249
|
+
res.writeHead(result.status || 200, headers);
|
|
206
250
|
res.end(result.body);
|
|
207
251
|
}
|
|
208
252
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.error(`ā API Error on /api/${routeName}:`, error);
|
|
253
|
+
} catch (err) {
|
|
254
|
+
console.error(`ā API Error /api/${routeName}:`, err);
|
|
212
255
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
213
|
-
res.end(JSON.stringify({ error:
|
|
256
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
214
257
|
}
|
|
215
258
|
}
|
|
216
259
|
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
+
};
|
|
230
283
|
|
|
231
|
-
// Notify all clients to reload
|
|
232
|
-
function notifyClients() {
|
|
233
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
234
|
-
console.log(`š [${timestamp}] Notifying ${clients.size} browser(s) to reload...`);
|
|
235
|
-
clients.forEach(client => {
|
|
236
|
-
try {
|
|
237
|
-
client.send('reload');
|
|
238
|
-
} catch (e) {
|
|
239
|
-
clients.delete(client);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Create HTTP server
|
|
245
284
|
const server = http.createServer(async (req, res) => {
|
|
246
|
-
|
|
247
|
-
|
|
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/')) {
|
|
248
296
|
await handleApiRequest(req, res);
|
|
249
297
|
return;
|
|
250
298
|
}
|
|
251
299
|
|
|
252
|
-
//
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
'Access-Control-Allow-Headers': 'Content-Type'
|
|
258
|
-
});
|
|
259
|
-
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));
|
|
260
305
|
return;
|
|
261
306
|
}
|
|
262
307
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
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;
|
|
267
329
|
}
|
|
268
330
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
'.png': 'image/png',
|
|
277
|
-
'.jpg': 'image/jpg',
|
|
278
|
-
'.gif': 'image/gif',
|
|
279
|
-
'.svg': 'image/svg+xml',
|
|
280
|
-
'.wav': 'audio/wav',
|
|
281
|
-
'.mp4': 'video/mp4',
|
|
282
|
-
'.woff': 'application/font-woff',
|
|
283
|
-
'.ttf': 'application/font-ttf',
|
|
284
|
-
'.eot': 'application/vnd.ms-fontobject',
|
|
285
|
-
'.otf': 'application/font-otf',
|
|
286
|
-
'.wasm': 'application/wasm'
|
|
287
|
-
};
|
|
331
|
+
// āā Static files
|
|
332
|
+
let filePath = path.join(ROOT, urlWithoutQuery);
|
|
333
|
+
|
|
334
|
+
// Default to index.html
|
|
335
|
+
if (urlWithoutQuery === '/' || urlWithoutQuery === '') {
|
|
336
|
+
filePath = path.join(ROOT, 'index.html');
|
|
337
|
+
}
|
|
288
338
|
|
|
289
|
-
|
|
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
|
+
}
|
|
290
344
|
|
|
291
|
-
fs.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
res.end('<h1>404 - File Not Found</h1>', 'utf-8');
|
|
296
|
-
} else {
|
|
297
|
-
res.writeHead(500);
|
|
298
|
-
res.end('Server Error: ' + error.code, 'utf-8');
|
|
299
|
-
}
|
|
345
|
+
if (!fs.existsSync(filePath)) {
|
|
346
|
+
// Try with .js extension
|
|
347
|
+
if (fs.existsSync(filePath + '.js')) {
|
|
348
|
+
filePath = filePath + '.js';
|
|
300
349
|
} else {
|
|
301
|
-
|
|
302
|
-
res.
|
|
303
|
-
|
|
304
|
-
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
305
|
-
'Pragma': 'no-cache',
|
|
306
|
-
'Expires': '0'
|
|
307
|
-
});
|
|
308
|
-
res.end(content, 'utf-8');
|
|
350
|
+
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
351
|
+
res.end('Not found: ' + urlWithoutQuery);
|
|
352
|
+
return;
|
|
309
353
|
}
|
|
310
|
-
}
|
|
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
|
+
}
|
|
311
374
|
});
|
|
312
375
|
|
|
313
|
-
// WebSocket
|
|
314
|
-
|
|
315
|
-
|
|
376
|
+
// āāā WebSocket (live reload) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
377
|
+
|
|
378
|
+
server.on('upgrade', (req, socket) => {
|
|
316
379
|
const key = req.headers['sec-websocket-key'];
|
|
317
|
-
const
|
|
318
|
-
.createHash('sha1')
|
|
380
|
+
const accept = crypto.createHash('sha1')
|
|
319
381
|
.update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
|
|
320
382
|
.digest('base64');
|
|
321
383
|
|
|
322
384
|
socket.write(
|
|
323
385
|
'HTTP/1.1 101 Switching Protocols\r\n' +
|
|
324
|
-
'Upgrade: websocket\r\n' +
|
|
325
|
-
|
|
326
|
-
`Sec-WebSocket-Accept: ${acceptKey}\r\n` +
|
|
327
|
-
'\r\n'
|
|
386
|
+
'Upgrade: websocket\r\nConnection: Upgrade\r\n' +
|
|
387
|
+
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`
|
|
328
388
|
);
|
|
329
389
|
|
|
330
|
-
const timestamp = new Date().toLocaleTimeString();
|
|
331
|
-
console.log(`š [${timestamp}] Browser connected (${clients.size + 1} total)`);
|
|
332
|
-
|
|
333
|
-
// Wrap socket with send method
|
|
334
390
|
const client = {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
frame
|
|
340
|
-
frame[1] = buffer.length;
|
|
341
|
-
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);
|
|
342
396
|
socket.write(frame);
|
|
343
397
|
}
|
|
344
398
|
};
|
|
345
399
|
|
|
346
400
|
clients.add(client);
|
|
401
|
+
console.log(`š Browser connected (${clients.size} total)`);
|
|
347
402
|
|
|
348
|
-
socket.on('close', () => {
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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 āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
353
413
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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);
|
|
357
436
|
});
|
|
358
437
|
});
|
|
359
438
|
|
|
360
|
-
//
|
|
439
|
+
// āāā Start āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
440
|
+
|
|
361
441
|
function startServer(port) {
|
|
362
442
|
server.listen(port, () => {
|
|
363
|
-
console.log(
|
|
364
|
-
console.log(
|
|
365
|
-
|
|
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 => {
|
|
366
447
|
if (err.code === 'EADDRINUSE') {
|
|
367
|
-
console.log(`ā ļø Port ${port}
|
|
448
|
+
console.log(`ā ļø Port ${port} busy, trying ${port + 1}...`);
|
|
368
449
|
startServer(port + 1);
|
|
369
450
|
} else {
|
|
370
|
-
console.error('ā
|
|
451
|
+
console.error('ā', err);
|
|
371
452
|
}
|
|
372
453
|
});
|
|
373
454
|
}
|
|
374
455
|
|
|
375
|
-
// Watch directories
|
|
376
|
-
const watchDirs = ['api', 'components', 'pages', 'modules', 'styles'];
|
|
377
|
-
|
|
378
|
-
// Debounce to prevent multiple rapid reloads
|
|
379
|
-
let reloadTimeout = null;
|
|
380
|
-
|
|
381
|
-
console.log('š Watching for changes in:');
|
|
382
|
-
watchDirs.forEach(dir => {
|
|
383
|
-
if (fs.existsSync(dir)) {
|
|
384
|
-
console.log(` - ${dir}/`);
|
|
385
|
-
fs.watch(dir, { recursive: false }, (eventType, filename) => {
|
|
386
|
-
if (filename && (filename.endsWith('.js') || filename.endsWith('.jsx') || filename.endsWith('.css'))) {
|
|
387
|
-
console.log(`\nš Detected change: ${dir}/${filename}`);
|
|
388
|
-
|
|
389
|
-
// Clear existing timeout
|
|
390
|
-
if (reloadTimeout) {
|
|
391
|
-
clearTimeout(reloadTimeout);
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
// Reload API routes if api/ folder changed
|
|
395
|
-
if (dir === 'api' && filename.endsWith('.js')) {
|
|
396
|
-
console.log('š Reloading API routes...');
|
|
397
|
-
loadApiRoutes();
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
// For CSS changes, just reload without regenerating imports
|
|
401
|
-
if (filename.endsWith('.css')) {
|
|
402
|
-
reloadTimeout = setTimeout(() => {
|
|
403
|
-
notifyClients();
|
|
404
|
-
}, 300);
|
|
405
|
-
} else {
|
|
406
|
-
// For JS/JSX changes, regenerate imports
|
|
407
|
-
reloadTimeout = setTimeout(() => {
|
|
408
|
-
generateImports();
|
|
409
|
-
}, 500);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
|
|
416
|
-
console.log('\nā
Dev server running with live reload!');
|
|
417
|
-
console.log('š” Create/modify files and browser will auto-refresh');
|
|
418
|
-
console.log('ā¹ļø Press Ctrl+C to stop\n');
|
|
419
|
-
|
|
420
|
-
// Load API routes
|
|
421
456
|
loadApiRoutes();
|
|
422
|
-
|
|
423
|
-
// Start server
|
|
424
457
|
startServer(PORT);
|
|
425
|
-
|
|
426
|
-
// Generate imports on startup
|
|
427
|
-
generateImports();
|
|
428
|
-
|
|
429
|
-
// Keep the process running
|
|
430
458
|
process.stdin.resume();
|