free-framework 4.4.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +198 -0
  3. package/bin/free.js +118 -0
  4. package/cli/commands/build.js +124 -0
  5. package/cli/commands/deploy.js +143 -0
  6. package/cli/commands/devtools.js +210 -0
  7. package/cli/commands/doctor.js +72 -0
  8. package/cli/commands/install.js +28 -0
  9. package/cli/commands/make.js +74 -0
  10. package/cli/commands/migrate.js +67 -0
  11. package/cli/commands/new.js +54 -0
  12. package/cli/commands/serve.js +73 -0
  13. package/cli/commands/test.js +57 -0
  14. package/compiler/analyzer.js +102 -0
  15. package/compiler/generator.js +386 -0
  16. package/compiler/lexer.js +166 -0
  17. package/compiler/parser.js +410 -0
  18. package/database/model.js +6 -0
  19. package/database/orm.js +379 -0
  20. package/database/query-builder.js +179 -0
  21. package/index.js +51 -0
  22. package/package.json +80 -0
  23. package/plugins/auth.js +212 -0
  24. package/plugins/cache.js +85 -0
  25. package/plugins/chat.js +32 -0
  26. package/plugins/mail.js +53 -0
  27. package/plugins/metrics.js +126 -0
  28. package/plugins/payments.js +59 -0
  29. package/plugins/queue.js +139 -0
  30. package/plugins/search.js +51 -0
  31. package/plugins/storage.js +123 -0
  32. package/plugins/upload.js +62 -0
  33. package/router/router.js +57 -0
  34. package/runtime/app.js +14 -0
  35. package/runtime/client.js +254 -0
  36. package/runtime/cluster.js +35 -0
  37. package/runtime/edge.js +62 -0
  38. package/runtime/middleware/maintenance.js +54 -0
  39. package/runtime/middleware/security.js +30 -0
  40. package/runtime/server.js +130 -0
  41. package/runtime/validator.js +102 -0
  42. package/runtime/views/error.free +104 -0
  43. package/runtime/views/maintenance.free +0 -0
  44. package/template-engine/renderer.js +24 -0
  45. package/templates/app-template/.env +23 -0
  46. package/templates/app-template/app/Exceptions/Handler.js +65 -0
  47. package/templates/app-template/app/Http/Controllers/AuthController.free +91 -0
  48. package/templates/app-template/app/Http/Middleware/AuthGuard.js +46 -0
  49. package/templates/app-template/app/Services/Storage.js +75 -0
  50. package/templates/app-template/app/Services/Validator.js +91 -0
  51. package/templates/app-template/app/controllers/AuthController.free +42 -0
  52. package/templates/app-template/app/middleware/auth.js +25 -0
  53. package/templates/app-template/app/models/User.free +32 -0
  54. package/templates/app-template/app/routes.free +12 -0
  55. package/templates/app-template/app/styles.css +23 -0
  56. package/templates/app-template/app/views/counter.free +23 -0
  57. package/templates/app-template/app/views/header.free +13 -0
  58. package/templates/app-template/config/app.js +32 -0
  59. package/templates/app-template/config/auth.js +39 -0
  60. package/templates/app-template/config/database.js +54 -0
  61. package/templates/app-template/package.json +28 -0
  62. package/templates/app-template/resources/css/app.css +11 -0
  63. package/templates/app-template/resources/views/dashboard.free +25 -0
  64. package/templates/app-template/resources/views/home.free +26 -0
  65. package/templates/app-template/routes/api.free +22 -0
  66. package/templates/app-template/routes/web.free +25 -0
  67. package/templates/app-template/tailwind.config.js +21 -0
  68. package/templates/app-template/views/about.ejs +47 -0
  69. package/templates/app-template/views/home.ejs +52 -0
  70. package/templates/auth/login.html +144 -0
  71. package/templates/auth/register.html +146 -0
  72. package/utils/logger.js +20 -0
@@ -0,0 +1,254 @@
1
+ /**
2
+ * runtime/client.js
3
+ * Ultra-fast Islands Hydration & SPA Engine.
4
+ * Powered by Free partial rendering.
5
+ */
6
+
7
+ (function () {
8
+ console.log("💎 Free Ultra Engine Activated");
9
+
10
+ const root = document.getElementById('free-app-root');
11
+ const loadingBar = document.createElement('div');
12
+ loadingBar.id = 'free-loader';
13
+ Object.assign(loadingBar.style, {
14
+ position: 'fixed',
15
+ top: '0',
16
+ left: '0',
17
+ height: '3px',
18
+ width: '0%',
19
+ backgroundColor: '#00ff88',
20
+ zIndex: '10000',
21
+ transition: 'width 0.3s ease, opacity 0.3s ease',
22
+ boxShadow: '0 0 10px #00ff88'
23
+ });
24
+ document.body.appendChild(loadingBar);
25
+
26
+ function setLoader(percent) {
27
+ loadingBar.style.opacity = '1';
28
+ loadingBar.style.width = percent + '%';
29
+ if (percent >= 100) {
30
+ setTimeout(() => {
31
+ loadingBar.style.opacity = '0';
32
+ setTimeout(() => loadingBar.style.width = '0%', 300);
33
+ }, 300);
34
+ }
35
+ }
36
+
37
+ async function navigate(url, push = true) {
38
+ setLoader(30);
39
+ try {
40
+ const response = await fetch(url, {
41
+ headers: { 'X-Free-Partial': 'true' }
42
+ });
43
+ setLoader(70);
44
+
45
+ if (!response.ok) throw new Error('Navigation failed');
46
+
47
+ const data = await response.json();
48
+
49
+ // SPA Transition: Fade out content
50
+ root.style.opacity = '0';
51
+ root.style.transform = 'translateY(10px)';
52
+ root.style.transition = 'opacity 0.2s ease, transform 0.2s ease';
53
+
54
+ setTimeout(() => {
55
+ root.innerHTML = data.content;
56
+ document.title = data.title;
57
+ if (push) history.pushState({ url }, data.title, url);
58
+
59
+ // Hydrate new islands
60
+ hydrate();
61
+
62
+ // Fade back in
63
+ root.style.opacity = '1';
64
+ root.style.transform = 'translateY(0)';
65
+ setLoader(100);
66
+ window.scrollTo(0, 0);
67
+ }, 200);
68
+
69
+ } catch (err) {
70
+ console.error("SPA Error:", err);
71
+ window.location.href = url; // Fallback
72
+ }
73
+ }
74
+
75
+ function hydrate() {
76
+ const islands = document.querySelectorAll('.free-component[data-component]');
77
+ islands.forEach(el => {
78
+ if (el.getAttribute('data-hydrated')) return;
79
+
80
+ const name = el.getAttribute('data-component');
81
+ const state = JSON.parse(el.getAttribute('data-state') || '{}');
82
+ const onMountCode = el.getAttribute('data-free-on-mount');
83
+ const onDestroyCode = el.getAttribute('data-free-on-destroy');
84
+
85
+ // 1. Run onMount
86
+ if (onMountCode && window.__free_actions && window.__free_actions[name] && window.__free_actions[name][onMountCode]) {
87
+ try { window.__free_actions[name][onMountCode](state); } catch (e) { console.error(`[onMount] ${name}:`, e); }
88
+ }
89
+
90
+ // 2. Attach event listeners
91
+ el.querySelectorAll('*').forEach(item => {
92
+ for (const attr of item.attributes) {
93
+ if (attr.name.startsWith('data-on-')) {
94
+ const eventName = attr.name.replace('data-on-', '');
95
+ const action = attr.value;
96
+
97
+ item.addEventListener(eventName, (event) => {
98
+ const actionId = attr.value;
99
+ if (window.__free_actions && window.__free_actions[name] && window.__free_actions[name][actionId]) {
100
+ try {
101
+ window.__free_actions[name][actionId](state, event);
102
+
103
+ // Update global state attribute for synchronization
104
+ el.setAttribute('data-state', JSON.stringify(state));
105
+
106
+ // Optionally re-render simple text bindings as a quick reactive measure
107
+ // Since we don't know exactly what changed, we'd need a VDOM to be perfect.
108
+ // For now, simple text node updates logic if needed...
109
+ } catch (e) {
110
+ console.error(`[Action] ${name} (${eventName}):`, e);
111
+ }
112
+ } else {
113
+ console.warn(`Missing action handler for ${actionId} on ${eventName}`);
114
+ }
115
+ });
116
+ }
117
+ }
118
+ });
119
+
120
+ el.setAttribute('data-hydrated', 'true');
121
+
122
+ if (onDestroyCode) {
123
+ const observer = new MutationObserver((mutations) => {
124
+ mutations.forEach((mutation) => {
125
+ mutation.removedNodes.forEach((node) => {
126
+ if (node === el) {
127
+ if (window.__free_actions && window.__free_actions[name] && window.__free_actions[name][onDestroyCode]) {
128
+ try { window.__free_actions[name][onDestroyCode](state); } catch (e) { console.error(`[onDestroy] ${name}:`, e); }
129
+ }
130
+ observer.disconnect();
131
+ }
132
+ });
133
+ });
134
+ });
135
+ observer.observe(el.parentNode, { childList: true });
136
+ }
137
+ });
138
+ }
139
+
140
+ // SPA Router interceptor for Links
141
+ document.addEventListener('click', (e) => {
142
+ const link = e.target.closest('a');
143
+ if (link && link.href && link.href.startsWith(window.location.origin)) {
144
+ const url = new URL(link.href);
145
+ // Check if it's the same page but different hash? Or just standard link
146
+ if (url.pathname !== window.location.pathname || url.search !== window.location.search) {
147
+ e.preventDefault();
148
+ navigate(link.href);
149
+ }
150
+ }
151
+ });
152
+
153
+ // SPA Router interceptor for Forms
154
+ document.addEventListener('submit', async (e) => {
155
+ const form = e.target;
156
+ if (form && form.action && form.action.startsWith(window.location.origin)) {
157
+ e.preventDefault();
158
+ setLoader(30);
159
+
160
+ const formData = new FormData(form);
161
+ const data = Object.fromEntries(formData.entries());
162
+
163
+ try {
164
+ const response = await fetch(form.action, {
165
+ method: form.method || 'POST',
166
+ headers: { 'Content-Type': 'application/json', 'X-Free-Partial': 'true' },
167
+ body: JSON.stringify(data),
168
+ redirect: 'follow'
169
+ });
170
+ setLoader(70);
171
+
172
+ if (response.redirected) {
173
+ navigate(response.url);
174
+ return;
175
+ }
176
+
177
+ if (!response.ok) {
178
+ // Try to extract an error message
179
+ let errMsg = "An error occurred";
180
+ try {
181
+ const errData = await response.json();
182
+ errMsg = errData.error || errMsg;
183
+ } catch (je) { }
184
+
185
+ // If it's the login form specifically, redirect with error
186
+ if (form.action.includes('/api/login')) {
187
+ navigate('/login?error=' + encodeURIComponent(errMsg));
188
+ } else {
189
+ navigate(window.location.pathname + "?error=" + encodeURIComponent(errMsg));
190
+ }
191
+ return;
192
+ }
193
+
194
+ // If response is OK and HTML content
195
+ try {
196
+ const resData = await response.json();
197
+ if (resData && resData.content) {
198
+ root.style.opacity = '0';
199
+ setTimeout(() => {
200
+ root.innerHTML = resData.content;
201
+ if (resData.title) document.title = resData.title;
202
+ hydrate();
203
+ root.style.opacity = '1';
204
+ setLoader(100);
205
+ }, 200);
206
+ } else if (resData && resData.success) {
207
+ // Action executed successfully, reload current route
208
+ navigate(window.location.pathname + window.location.search);
209
+ }
210
+ } catch (e) {
211
+ // Fallback reload
212
+ window.location.reload();
213
+ }
214
+
215
+ } catch (err) {
216
+ console.error("Form submit failed:", err);
217
+ form.submit(); // Fallback to synchronous hard submit
218
+ }
219
+ }
220
+ }, true);
221
+
222
+ window.addEventListener('popstate', (e) => {
223
+ if (e.state && e.state.url) {
224
+ navigate(e.state.url, false);
225
+ }
226
+ });
227
+
228
+ // Global Free helper
229
+ window.Free = {
230
+ navigate,
231
+ async call(actionName, data = {}) {
232
+ const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
233
+ try {
234
+ const response = await fetch(`/_free/action/${actionName}`, {
235
+ method: 'POST',
236
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
237
+ body: JSON.stringify(data)
238
+ });
239
+ const result = await response.json();
240
+ if (!response.ok) throw new Error(result.error || 'Action failed');
241
+ return result;
242
+ } catch (err) {
243
+ console.error(`❌ Action Error [${actionName}]:`, err.message);
244
+ throw err;
245
+ }
246
+ }
247
+ };
248
+
249
+ if (document.readyState === 'loading') {
250
+ document.addEventListener('DOMContentLoaded', hydrate);
251
+ } else {
252
+ hydrate();
253
+ }
254
+ })();
@@ -0,0 +1,35 @@
1
+ /**
2
+ * runtime/cluster.js
3
+ * Multi-Core Clustering Engine for Free Ultra
4
+ * Forks the server process across all available CPU cores for maximum throughput.
5
+ */
6
+
7
+ const cluster = require('cluster');
8
+ const os = require('os');
9
+
10
+ class ClusterManager {
11
+ static ignite(startServerCallback) {
12
+ if (cluster.isMaster || cluster.isPrimary) {
13
+ const numCPUs = os.cpus().length;
14
+ console.log(`\n🛡️ Free Engine Cluster Manager Intercepting...`);
15
+ console.log(`🔥 Igniting ${numCPUs} Worker Processes natively...\n`);
16
+
17
+ for (let i = 0; i < numCPUs; i++) {
18
+ cluster.fork();
19
+ }
20
+
21
+ cluster.on('exit', (worker, code, signal) => {
22
+ console.error(`❌ Worker ${worker.process.pid} died (Code: ${code}). Resurrecting...`);
23
+ cluster.fork(); // Auto-healing
24
+ });
25
+
26
+ console.log(`✅ Master Process PID: ${process.pid} is routing traffic.`);
27
+ } else {
28
+ // Workers share the TCP connection in Node.js Cluster mode
29
+ startServerCallback();
30
+ console.log(` └─ Worker PID: ${process.pid} ready for combat.`);
31
+ }
32
+ }
33
+ }
34
+
35
+ module.exports = { ClusterManager };
@@ -0,0 +1,62 @@
1
+ /**
2
+ * runtime/edge.js
3
+ * Edge computing adaptor for Cloudflare Workers and Bun environment handling.
4
+ * Avoids Node-specific standard library where necessary.
5
+ */
6
+
7
+ // A generic Request/Response handler abstraction matching Cloudflare Worker signature
8
+ class EdgeRuntime {
9
+ constructor(freeApp) {
10
+ this.app = freeApp;
11
+ }
12
+
13
+ async fetch(request, env, ctx) {
14
+ const url = new URL(request.url);
15
+ const path = url.pathname;
16
+ const method = request.method;
17
+
18
+ // Abstracted Edge request transformation
19
+ const mockReq = {
20
+ method: method,
21
+ path: path,
22
+ url: request.url,
23
+ headers: Object.fromEntries(request.headers.entries()),
24
+ text: async () => await request.text(),
25
+ json: async () => await request.json()
26
+ };
27
+
28
+ const mockRes = {
29
+ statusCode: 200,
30
+ headers: new Headers(),
31
+ body: null,
32
+ status: function (code) { this.statusCode = code; return this; },
33
+ header: function (k, v) { this.headers.set(k, v); return this; },
34
+ send: function (data) { this.body = data; },
35
+ json: function (data) {
36
+ this.headers.set('Content-Type', 'application/json');
37
+ this.body = JSON.stringify(data);
38
+ }
39
+ };
40
+
41
+ // Note: Edge compatibility implies passing through mocked req/res to HyperExpress routing tree logic,
42
+ // or substituting HyperExpress entirely if running in CF workers (which lacks TCP sockets).
43
+ // This is the adaptor structural foundation for the routing engine.
44
+ // For Bun, we could utilize native Bun.serve().
45
+
46
+ try {
47
+ // Simulated minimal routing execution hook (would integrate with generator AST)
48
+ if (path.startsWith('/_free/action/')) {
49
+ // handle actions dynamically without node streams
50
+ }
51
+
52
+ return new Response(mockRes.body || "Free Edge Runtime active", {
53
+ status: mockRes.statusCode,
54
+ headers: mockRes.headers
55
+ });
56
+ } catch (error) {
57
+ return new Response("Internal Edge Error", { status: 500 });
58
+ }
59
+ }
60
+ }
61
+
62
+ module.exports = { EdgeRuntime };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * runtime/middleware/maintenance.js
3
+ * Enterprise Maintenance Mode Guard.
4
+ */
5
+
6
+ module.exports = function maintenanceMiddleware(req, res, next) {
7
+ const isMaintenance = process.env.MAINTENANCE_MODE === 'true';
8
+ const bypassToken = process.env.MAINTENANCE_BYPASS;
9
+
10
+ // Allow bypassing with a specific query token or if disabled
11
+ if (!isMaintenance || (bypassToken && req.query.bypass === bypassToken)) {
12
+ return next();
13
+ }
14
+
15
+ // Identify if it's an API request or HTML
16
+ const acceptsHtml = req.headers['accept']?.includes('text/html');
17
+
18
+ if (acceptsHtml) {
19
+ // Render the high-end Maintenance page from the registry
20
+ // Note: The registry is attached to the server instance (res.app in HyperExpress context, but we need the server)
21
+ // For now, we'll return a premium standalone HTML fallback if the registry isn't easily accessible
22
+ res.status(503).send(`
23
+ <!DOCTYPE html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="UTF-8">
27
+ <title>System Evolution | Free Ultra</title>
28
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700;900&display=swap" rel="stylesheet">
29
+ <style>
30
+ body { margin:0; font-family:'Outfit',sans-serif; background:#050505; color:white; overflow:hidden; }
31
+ .center { height:100vh; display:flex; align-items:center; justify-content:center; flex-direction:column; text-align:center; }
32
+ .glow { width:400px; height:400px; background:radial-gradient(circle, rgba(0,255,136,0.15) 0%, transparent 70%); position:absolute; z-index:-1; animation: pulse 4s infinite; }
33
+ h1 { font-size:4rem; font-weight:900; letter-spacing:-2px; margin:0; }
34
+ p { color:#888; font-size:1.2rem; max-width:500px; line-height:1.6; }
35
+ @keyframes pulse { 0%, 100% { transform: scale(1); opacity:0.5; } 50% { transform: scale(1.2); opacity:0.8; } }
36
+ </style>
37
+ </head>
38
+ <body>
39
+ <div class="center">
40
+ <div class="glow"></div>
41
+ <h1>SYSTEM <span style="color:#00ff88">EVOLUTION</span></h1>
42
+ <p style="margin-top:20px;">We're currently upgrading the core engine to provide unprecedented performance and security. We'll be back online shortly.</p>
43
+ </div>
44
+ </body>
45
+ </html>
46
+ `);
47
+ } else {
48
+ res.status(503).json({
49
+ success: false,
50
+ status: 'maintenance',
51
+ message: 'Enterprise System is under scheduled maintenance.'
52
+ });
53
+ }
54
+ };
@@ -0,0 +1,30 @@
1
+ /**
2
+ * runtime/middleware/security.js
3
+ * Enterprise-grade Security Headers Middleware.
4
+ */
5
+
6
+ module.exports = function securityMiddleware(req, res, next) {
7
+ // 1. Core Security Headers
8
+ res.setHeader('X-Powered-By', 'Free-Ultra/Enterprise');
9
+ res.setHeader('X-Content-Type-Options', 'nosniff');
10
+ res.setHeader('X-Frame-Options', 'DENY');
11
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
12
+
13
+ // 2. Content Security Policy (Optimized for Free runtime)
14
+ res.setHeader('Content-Security-Policy',
15
+ "default-src 'self'; " +
16
+ "script-src 'self' 'unsafe-inline' blob: https://cdn.jsdelivr.net; " +
17
+ "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; " +
18
+ "font-src 'self' data: https://fonts.gstatic.com; " +
19
+ "img-src 'self' data: blob:;"
20
+ );
21
+
22
+ // 3. Modern Browser Protections
23
+ res.setHeader('X-XSS-Protection', '1; mode=block');
24
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
25
+
26
+ // 4. Permissions Policy (Hardware restrictions)
27
+ res.setHeader('Permissions-Policy', 'camera=(), microphone=(), geolocation=(), interest-cohort=()');
28
+
29
+ next();
30
+ };
@@ -0,0 +1,130 @@
1
+ /**
2
+ * runtime/server.js
3
+ * Core server class for the Free Engine.
4
+ * Powered by HyperExpress (uWebSockets.js) for maximum performance.
5
+ */
6
+
7
+ const HyperExpress = require('hyper-express');
8
+ const nodePath = require('path');
9
+ const fs = require('fs');
10
+ const crypto = require('crypto');
11
+ const securityMiddleware = require('./middleware/security');
12
+ const maintenanceMiddleware = require('./middleware/maintenance');
13
+
14
+ const { LRUCache } = require('lru-cache');
15
+
16
+ class FreeServer {
17
+ constructor() {
18
+ this.app = new HyperExpress.Server();
19
+ this.namedMiddlewares = {};
20
+ this.errorViews = {};
21
+ this.plugins = [];
22
+ this.viewsPath = process.env.VIEWS_PATH || nodePath.join(process.cwd(), 'views');
23
+
24
+ // Static Asset Serving - GET fallback for priority
25
+ const publicPath = nodePath.join(process.cwd(), 'public');
26
+ this.app.get('/*', (req, res, next) => {
27
+ const lookupPath = req.path.startsWith('/') ? req.path.substring(1) : req.path;
28
+ if (!lookupPath) return next();
29
+
30
+ const fullPath = nodePath.join(publicPath, lookupPath);
31
+ if (fs.existsSync(fullPath) && fs.statSync(fullPath).isFile()) {
32
+ const content = fs.readFileSync(fullPath);
33
+ const ext = nodePath.extname(fullPath);
34
+ const mimes = { '.js': 'application/javascript', '.css': 'text/css', '.png': 'image/png' };
35
+ return res.type(mimes[ext] || 'application/javascript').send(content);
36
+ }
37
+ next();
38
+ });
39
+
40
+ this.app.get('/test-static', (req, res) => res.send('Static serving test'));
41
+
42
+ // Extreme SSR Cache (LRU)
43
+ this.cache = new LRUCache({
44
+ max: 500,
45
+ ttl: 1000 * 60 * 5,
46
+ });
47
+
48
+ // 2. Ultra-Fast InMemory Rate Limiting (DDoS Shield)
49
+ this.rateLimits = new LRUCache({
50
+ max: 100000,
51
+ ttl: 1000 * 60,
52
+ });
53
+
54
+ // Apply Global Middleware
55
+ this.app.use(maintenanceMiddleware);
56
+ this.app.use(securityMiddleware);
57
+
58
+ this.app.use((req, res, next) => {
59
+ const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
60
+ const reqCount = this.rateLimits.get(ip) || 0;
61
+
62
+ if (reqCount >= 5000) {
63
+ res.status(429);
64
+ res.setHeader('Retry-After', '60');
65
+ return res.send('Too Many Requests - Under DDoS Protection');
66
+ }
67
+
68
+ this.rateLimits.set(ip, reqCount + 1);
69
+ next();
70
+ });
71
+ }
72
+
73
+ route(method, path, middlewares = [], handler) {
74
+ const routeMethod = method.toLowerCase();
75
+ const mwFns = middlewares.map(m => this.namedMiddlewares[m]).filter(Boolean);
76
+
77
+ const routeHandler = async (req, res) => {
78
+ try {
79
+ const cacheKey = `${method}:${req.url}`;
80
+ if (this.cache.has(cacheKey)) {
81
+ res.header('X-Free-Cache', 'HIT');
82
+ res.header('Content-Type', 'text/html');
83
+ return res.send(this.cache.get(cacheKey));
84
+ }
85
+
86
+ res.header('X-Free-Cache', 'MISS');
87
+ res.context = res.context || {};
88
+ res.context.props = {
89
+ params: req.path_parameters || {},
90
+ query: req.query_parameters || {},
91
+ };
92
+
93
+ const originalSend = res.send.bind(res);
94
+ res.send = (body) => {
95
+ if (res.statusCode === 200) this.cache.set(cacheKey, body);
96
+ return originalSend(body);
97
+ };
98
+
99
+ await handler(req, res);
100
+ } catch (e) {
101
+ console.error(`[Free Engine] Error ${method} ${path}:`, e);
102
+ res.status(e.status || 500).header('Content-Type', 'text/html').send('Internal Error');
103
+ }
104
+ };
105
+
106
+ this.app[routeMethod](path, ...mwFns, routeHandler);
107
+ if (routeMethod === 'get') this.app.head(path, ...mwFns, routeHandler);
108
+ }
109
+
110
+ middleware(fn) { this.app.use(fn); }
111
+ registerMiddleware(name, fn) { this.namedMiddlewares[name] = fn; }
112
+ use(plugin) { plugin.install(this); this.plugins.push(plugin); }
113
+
114
+ setErrorView(code, view) {
115
+ this.app.set_not_found_handler((req, res) => {
116
+ if (code.toString() === '404') {
117
+ res.status(404).send(`<!-- error ${view} -->`);
118
+ }
119
+ });
120
+ }
121
+
122
+ start(port = 3000) {
123
+ this.app.get('/health', (req, res) => res.send('OK'));
124
+ this.app.listen(port).then(() => {
125
+ console.log(`⚡ Free Engine Ignite: http://localhost:${port}`);
126
+ });
127
+ }
128
+ }
129
+
130
+ module.exports = { FreeServer };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * runtime/validator.js
3
+ * Built-in request validation for Free Framework.
4
+ * Supports: required, email, min:N, max:N, string, number, boolean, url, regex.
5
+ */
6
+
7
+ const RULES = {
8
+ required: (val) => val !== undefined && val !== null && val !== '',
9
+ email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(String(val)),
10
+ string: (val) => typeof val === 'string',
11
+ number: (val) => !isNaN(Number(val)) && val !== '',
12
+ boolean: (val) => val === 'true' || val === 'false' || typeof val === 'boolean',
13
+ url: (val) => { try { new URL(val); return true; } catch { return false; } },
14
+ min: (val, param) => String(val).length >= parseInt(param),
15
+ max: (val, param) => String(val).length <= parseInt(param),
16
+ regex: (val, param) => new RegExp(param).test(String(val)),
17
+ };
18
+
19
+ const MESSAGES = {
20
+ required: (field) => `${field} is required.`,
21
+ email: (field) => `${field} must be a valid email address.`,
22
+ string: (field) => `${field} must be a string.`,
23
+ number: (field) => `${field} must be a number.`,
24
+ boolean: (field) => `${field} must be a boolean.`,
25
+ url: (field) => `${field} must be a valid URL.`,
26
+ min: (field, param) => `${field} must be at least ${param} characters.`,
27
+ max: (field, param) => `${field} must be at most ${param} characters.`,
28
+ regex: (field) => `${field} is not in the correct format.`,
29
+ };
30
+
31
+ /**
32
+ * Validate a data object against a rules map.
33
+ * @param {Object} data - { field: value }
34
+ * @param {Object} rules - { field: ['required', 'email', 'min:8'] }
35
+ * @returns {{ valid: boolean, errors: { field: string[] } }}
36
+ */
37
+ function validate(data, rules) {
38
+ const errors = {};
39
+
40
+ for (const [field, fieldRules] of Object.entries(rules)) {
41
+ const value = data[field];
42
+ const fieldErrors = [];
43
+
44
+ for (const rule of fieldRules) {
45
+ const [ruleName, ruleParam] = rule.split(':');
46
+ const ruleHandler = RULES[ruleName];
47
+
48
+ if (!ruleHandler) {
49
+ console.warn(`[Free Validator] Unknown rule: "${ruleName}"`);
50
+ continue;
51
+ }
52
+
53
+ // Skip non-required rules if the field is empty (only fail on 'required')
54
+ if (ruleName !== 'required' && (value === undefined || value === null || value === '')) {
55
+ continue;
56
+ }
57
+
58
+ const passed = ruleHandler(value, ruleParam);
59
+ if (!passed) {
60
+ const msgFn = MESSAGES[ruleName] || (() => `${field} failed ${ruleName} validation.`);
61
+ fieldErrors.push(msgFn(field, ruleParam));
62
+ }
63
+ }
64
+
65
+ if (fieldErrors.length > 0) {
66
+ errors[field] = fieldErrors;
67
+ }
68
+ }
69
+
70
+ const valid = Object.keys(errors).length === 0;
71
+ return { valid, errors };
72
+ }
73
+
74
+ /**
75
+ * Create an Express/HyperExpress middleware from a rules map.
76
+ * Returns 422 JSON response if validation fails.
77
+ */
78
+ function validationMiddleware(rules) {
79
+ return async (req, res, next) => {
80
+ let body = {};
81
+ try {
82
+ body = await req.json();
83
+ } catch {
84
+ try { body = await req.urlencoded(); } catch { body = {}; }
85
+ }
86
+
87
+ const { valid, errors } = validate(body, rules);
88
+
89
+ if (!valid) {
90
+ return res.status(422).json({
91
+ success: false,
92
+ message: 'Validation failed.',
93
+ errors,
94
+ });
95
+ }
96
+
97
+ req.validated = body;
98
+ next();
99
+ };
100
+ }
101
+
102
+ module.exports = { validate, validationMiddleware };