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.
- package/LICENSE +21 -0
- package/README.md +198 -0
- package/bin/free.js +118 -0
- package/cli/commands/build.js +124 -0
- package/cli/commands/deploy.js +143 -0
- package/cli/commands/devtools.js +210 -0
- package/cli/commands/doctor.js +72 -0
- package/cli/commands/install.js +28 -0
- package/cli/commands/make.js +74 -0
- package/cli/commands/migrate.js +67 -0
- package/cli/commands/new.js +54 -0
- package/cli/commands/serve.js +73 -0
- package/cli/commands/test.js +57 -0
- package/compiler/analyzer.js +102 -0
- package/compiler/generator.js +386 -0
- package/compiler/lexer.js +166 -0
- package/compiler/parser.js +410 -0
- package/database/model.js +6 -0
- package/database/orm.js +379 -0
- package/database/query-builder.js +179 -0
- package/index.js +51 -0
- package/package.json +80 -0
- package/plugins/auth.js +212 -0
- package/plugins/cache.js +85 -0
- package/plugins/chat.js +32 -0
- package/plugins/mail.js +53 -0
- package/plugins/metrics.js +126 -0
- package/plugins/payments.js +59 -0
- package/plugins/queue.js +139 -0
- package/plugins/search.js +51 -0
- package/plugins/storage.js +123 -0
- package/plugins/upload.js +62 -0
- package/router/router.js +57 -0
- package/runtime/app.js +14 -0
- package/runtime/client.js +254 -0
- package/runtime/cluster.js +35 -0
- package/runtime/edge.js +62 -0
- package/runtime/middleware/maintenance.js +54 -0
- package/runtime/middleware/security.js +30 -0
- package/runtime/server.js +130 -0
- package/runtime/validator.js +102 -0
- package/runtime/views/error.free +104 -0
- package/runtime/views/maintenance.free +0 -0
- package/template-engine/renderer.js +24 -0
- package/templates/app-template/.env +23 -0
- package/templates/app-template/app/Exceptions/Handler.js +65 -0
- package/templates/app-template/app/Http/Controllers/AuthController.free +91 -0
- package/templates/app-template/app/Http/Middleware/AuthGuard.js +46 -0
- package/templates/app-template/app/Services/Storage.js +75 -0
- package/templates/app-template/app/Services/Validator.js +91 -0
- package/templates/app-template/app/controllers/AuthController.free +42 -0
- package/templates/app-template/app/middleware/auth.js +25 -0
- package/templates/app-template/app/models/User.free +32 -0
- package/templates/app-template/app/routes.free +12 -0
- package/templates/app-template/app/styles.css +23 -0
- package/templates/app-template/app/views/counter.free +23 -0
- package/templates/app-template/app/views/header.free +13 -0
- package/templates/app-template/config/app.js +32 -0
- package/templates/app-template/config/auth.js +39 -0
- package/templates/app-template/config/database.js +54 -0
- package/templates/app-template/package.json +28 -0
- package/templates/app-template/resources/css/app.css +11 -0
- package/templates/app-template/resources/views/dashboard.free +25 -0
- package/templates/app-template/resources/views/home.free +26 -0
- package/templates/app-template/routes/api.free +22 -0
- package/templates/app-template/routes/web.free +25 -0
- package/templates/app-template/tailwind.config.js +21 -0
- package/templates/app-template/views/about.ejs +47 -0
- package/templates/app-template/views/home.ejs +52 -0
- package/templates/auth/login.html +144 -0
- package/templates/auth/register.html +146 -0
- package/utils/logger.js +20 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compiler/analyzer.js
|
|
3
|
+
* Static Analysis pass for Free Framework.
|
|
4
|
+
* Detects errors before runtime:
|
|
5
|
+
* - undefined views / pages
|
|
6
|
+
* - unknown middleware references
|
|
7
|
+
* - empty action bodies
|
|
8
|
+
* - orphaned models (declared but not used)
|
|
9
|
+
* - duplicate route paths
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
function analyze(ast, filename = 'app') {
|
|
13
|
+
const warnings = [];
|
|
14
|
+
const errors = [];
|
|
15
|
+
|
|
16
|
+
function warn(msg, line) {
|
|
17
|
+
warnings.push({ type: 'warning', file: filename, line: line || 0, message: msg });
|
|
18
|
+
}
|
|
19
|
+
function error(msg, line) {
|
|
20
|
+
errors.push({ type: 'error', file: filename, line: line || 0, message: msg });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Collect declared names (case-insensitive for comparison)
|
|
24
|
+
const declaredViews = new Set(ast.filter(n => n.type === 'page' || n.type === 'component').map(n => n.name.toLowerCase()));
|
|
25
|
+
const declaredModels = new Set(ast.filter(n => n.type === 'model').map(n => n.name));
|
|
26
|
+
const declaredActions = new Set(ast.filter(n => n.type === 'action').map(n => n.name));
|
|
27
|
+
|
|
28
|
+
const routePaths = new Map(); // path+method → line
|
|
29
|
+
|
|
30
|
+
ast.forEach(node => {
|
|
31
|
+
// ── Routes ──────────────────────────────────────────────────────────
|
|
32
|
+
if (node.type === 'route') {
|
|
33
|
+
// Check for undefined views
|
|
34
|
+
if (node.view && !declaredViews.has(node.view.toLowerCase())) {
|
|
35
|
+
warn(`Route "${node.method} ${node.path}" references undefined view "${node.view}". Did you forget to create ${node.view}.free?`, node.line);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Check for duplicate routes
|
|
39
|
+
const routeKey = `${node.method}:${node.path}`;
|
|
40
|
+
if (routePaths.has(routeKey)) {
|
|
41
|
+
error(`Duplicate route: "${node.method} ${node.path}" is defined more than once.`, node.line);
|
|
42
|
+
}
|
|
43
|
+
routePaths.set(routeKey, node.line);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Actions ──────────────────────────────────────────────────────────
|
|
47
|
+
if (node.type === 'action') {
|
|
48
|
+
if (!node.code || node.code.trim() === '') {
|
|
49
|
+
warn(`Action "${node.name}" has an empty body. It won't do anything.`, node.line);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Models ───────────────────────────────────────────────────────────
|
|
54
|
+
if (node.type === 'model') {
|
|
55
|
+
if (!node.fields || node.fields.length === 0) {
|
|
56
|
+
warn(`Model "${node.name}" has no fields defined.`, node.line);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Auth ──────────────────────────────────────────────────────────────
|
|
61
|
+
if (node.type === 'auth') {
|
|
62
|
+
if (!node.config.login && !node.config.register) {
|
|
63
|
+
warn(`"auth" block defined but neither login nor register paths are set.`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return { errors, warnings, clean: errors.length === 0 };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Format analysis results for CLI output.
|
|
73
|
+
*/
|
|
74
|
+
function printAnalysis({ errors, warnings, clean }, filename) {
|
|
75
|
+
const colors = {
|
|
76
|
+
reset: '\x1b[0m',
|
|
77
|
+
red: '\x1b[31m',
|
|
78
|
+
yellow: '\x1b[33m',
|
|
79
|
+
green: '\x1b[32m',
|
|
80
|
+
gray: '\x1b[90m',
|
|
81
|
+
bold: '\x1b[1m',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
85
|
+
console.log(`${colors.green}✅ ${filename}: No issues detected.${colors.reset}`);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
errors.forEach(e => {
|
|
90
|
+
console.error(`${colors.red}${colors.bold}ERROR${colors.reset} ${colors.gray}${e.file}:${e.line}${colors.reset} — ${e.message}`);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
warnings.forEach(w => {
|
|
94
|
+
console.warn(`${colors.yellow}WARN${colors.reset} ${colors.gray}${w.file}:${w.line}${colors.reset} — ${w.message}`);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!clean) {
|
|
98
|
+
console.error(`\n${colors.red}Found ${errors.length} error(s). Fix before building.${colors.reset}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { analyze, printAnalysis };
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compiler/generator.js
|
|
3
|
+
* Code Generator for Free Ultra (v4.3).
|
|
4
|
+
* Optimized for Islands Architecture and Component Composition.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
function generate(ast) {
|
|
8
|
+
let output = "require('dotenv').config();\n" +
|
|
9
|
+
"const { FreeServer } = require('free-framework/runtime/server');\n" +
|
|
10
|
+
"const { Model } = require('free-framework/database/model');\n" +
|
|
11
|
+
"const { ORM } = require('free-framework/database/orm');\n" +
|
|
12
|
+
"const { ClusterManager } = require('free-framework/runtime/cluster');\n" +
|
|
13
|
+
"const crypto = require('crypto');\n" +
|
|
14
|
+
"const bcrypt = require('bcryptjs');\n" +
|
|
15
|
+
"const jwt = require('jsonwebtoken');\n" +
|
|
16
|
+
"const fs = require('fs');\n" +
|
|
17
|
+
"const path = require('path');\n" +
|
|
18
|
+
"// Enterprise Services (Global Inject)\n" +
|
|
19
|
+
"let Validator, Storage;\n" +
|
|
20
|
+
"try { Validator = require(path.join(process.cwd(), 'app/Services/Validator.js')); } catch(e) {}\n" +
|
|
21
|
+
"try { Storage = require(path.join(process.cwd(), 'app/Services/Storage.js')); } catch(e) {}\n" +
|
|
22
|
+
"const server = new FreeServer();\n\n" +
|
|
23
|
+
"// Models\n" +
|
|
24
|
+
"const modelsRegistry = {};\n";
|
|
25
|
+
|
|
26
|
+
const CleanCSS = require('clean-css');
|
|
27
|
+
const glob = require('fs'); // Just using fs for readdirSync
|
|
28
|
+
|
|
29
|
+
const components = {};
|
|
30
|
+
const actions = {};
|
|
31
|
+
let globalCSS = "";
|
|
32
|
+
|
|
33
|
+
ast.forEach(node => {
|
|
34
|
+
if (node.type === 'action') actions[node.name] = node.code;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
ast.forEach(node => {
|
|
38
|
+
if (node.type === 'model') {
|
|
39
|
+
output += `class ${node.name} extends Model {}\n${node.name}.fields = ${JSON.stringify(node.fields)};\n`;
|
|
40
|
+
output += `modelsRegistry['${node.name}'] = ${node.name};\n`;
|
|
41
|
+
}
|
|
42
|
+
if (node.type === 'component') {
|
|
43
|
+
components[node.name] = node;
|
|
44
|
+
const styles = node.body.filter(n => n.type === 'style');
|
|
45
|
+
styles.forEach(s => {
|
|
46
|
+
globalCSS += `.free-component[data-component="${node.name}"] { ${s.content} }\n`;
|
|
47
|
+
globalCSS += `.free-component:not([data-component]) { ${s.content} }\n`;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
if (node.type === 'auth') {
|
|
51
|
+
const loginPath = node.config.login || '/login';
|
|
52
|
+
const registerPath = node.config.register || '/register';
|
|
53
|
+
output += `server.use(require('free-framework/plugins/auth')( ${JSON.stringify(node.config)} ));\n`;
|
|
54
|
+
|
|
55
|
+
output += `const loadAuthTemplate = (name, vars = {}) => {
|
|
56
|
+
const userTemplate = path.join(process.cwd(), 'resources/views/auth', name + '.html');
|
|
57
|
+
const coreTemplate = path.join(process.env.FREE_PATH || '', 'templates/auth', name + '.html');
|
|
58
|
+
let html = fs.existsSync(userTemplate) ? fs.readFileSync(userTemplate, 'utf8') : (fs.existsSync(coreTemplate) ? fs.readFileSync(coreTemplate, 'utf8') : 'Template not found');
|
|
59
|
+
Object.keys(vars).forEach(k => html = html.replace(new RegExp('{{' + k + '}}', 'g'), vars[k]));
|
|
60
|
+
return html;
|
|
61
|
+
};\n`;
|
|
62
|
+
|
|
63
|
+
output += `server.app.get('${loginPath}', (req, res) => {\n`;
|
|
64
|
+
output += ` res.header('Content-Type', 'text/html');\n`;
|
|
65
|
+
output += ` res.send(loadAuthTemplate('login', { registerPath: '${registerPath}' }));\n`;
|
|
66
|
+
output += `});\n`;
|
|
67
|
+
output += `server.app.get('${registerPath}', (req, res) => {\n`;
|
|
68
|
+
output += ` res.header('Content-Type', 'text/html');\n`;
|
|
69
|
+
output += ` res.send(loadAuthTemplate('register', { loginPath: '${loginPath}' }));\n`;
|
|
70
|
+
output += `});\n`;
|
|
71
|
+
}
|
|
72
|
+
if (node.type === 'upload') {
|
|
73
|
+
output += `server.use(require('free-framework/plugins/upload')('${node.name}', ${JSON.stringify(node.config)} ));\n`;
|
|
74
|
+
}
|
|
75
|
+
if (node.type === 'error') {
|
|
76
|
+
output += `server.setErrorView('${node.code}', '${node.view}');\n`;
|
|
77
|
+
}
|
|
78
|
+
if (node.type === 'validate') {
|
|
79
|
+
output += `server.registerMiddleware('validate:${node.model}', async (req, res, next) => {
|
|
80
|
+
const body = req.body || {};
|
|
81
|
+
const model = modelsRegistry['${node.model}'];
|
|
82
|
+
if (model && model.fields) {
|
|
83
|
+
for (const field of model.fields) {
|
|
84
|
+
if (field.required && !body[field.name]) {
|
|
85
|
+
return res.status(400).json({ success: false, error: \`Field \${field.name} is required\` });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const rules = async () => { ${node.rules} };
|
|
90
|
+
try { await rules(); next(); } catch(e) { res.status(400).json({ success: false, error: e.message }); }
|
|
91
|
+
});\n`;
|
|
92
|
+
}
|
|
93
|
+
if (node.type === 'store') {
|
|
94
|
+
output += `server.store = server.store || {};\nserver.store['${node.name}'] = (() => { ${node.body} })();\n`;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
output += `server.use(require('free-framework/plugins/chat')());\n`;
|
|
99
|
+
|
|
100
|
+
ast.forEach(node => {
|
|
101
|
+
if (node.type === 'action') {
|
|
102
|
+
output += `\nserver.app.post("/_free/action/${node.name}", async (req, res) => {
|
|
103
|
+
try {
|
|
104
|
+
const user = (res.context && res.context.user) ? res.context.user : null;
|
|
105
|
+
let body = {};
|
|
106
|
+
if (req.headers['content-type'] && req.headers['content-type'].includes('application/json')) {
|
|
107
|
+
body = await req.json().catch(() => ({}));
|
|
108
|
+
} else {
|
|
109
|
+
body = await req.urlencoded().catch(() => ({}));
|
|
110
|
+
}
|
|
111
|
+
const result = await (async () => {\n`;
|
|
112
|
+
if (node.params && node.params.length > 0) {
|
|
113
|
+
output += ` const { ${node.params.join(', ')} } = body;\n`;
|
|
114
|
+
}
|
|
115
|
+
output += " " + node.code.replace(/res\.locals/g, 'res.context') + "\n" +
|
|
116
|
+
" })();\n" +
|
|
117
|
+
" if (!res.headersSent) res.json({ success: true, result });\n" +
|
|
118
|
+
" } catch(e) {\n" +
|
|
119
|
+
" if (!res.headersSent) res.status(500).json({ success: false, error: e.message });\n" +
|
|
120
|
+
" }\n" +
|
|
121
|
+
"});\n";
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
globalCSS = new CleanCSS().minify(globalCSS).styles;
|
|
126
|
+
output += `\nconst scopedCSS = ${JSON.stringify(globalCSS.trim())};\n`;
|
|
127
|
+
|
|
128
|
+
output += `\nserver.renderRegistry = {\n`;
|
|
129
|
+
Object.keys(components).forEach((name, i) => {
|
|
130
|
+
output += ` ${name}: render${name}${i === Object.keys(components).length - 1 ? '' : ','}\n`;
|
|
131
|
+
});
|
|
132
|
+
output += `};\n`;
|
|
133
|
+
|
|
134
|
+
output += `\nserver.middleware((req, res, next) => {
|
|
135
|
+
if (!res.context) res.context = {};
|
|
136
|
+
res.context.csrfToken = crypto.randomBytes(32).toString('hex');
|
|
137
|
+
next();
|
|
138
|
+
});\n`;
|
|
139
|
+
|
|
140
|
+
// Actually emit the component rendering functions (hoisted)
|
|
141
|
+
Object.values(components).forEach(node => {
|
|
142
|
+
output += generateUltraComponent(node);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// ── Auto-register Custom Middlewares from app/middleware ─────────────────
|
|
146
|
+
output += `
|
|
147
|
+
const mwDir = path.join(process.cwd(), 'app/middleware');
|
|
148
|
+
if (fs.existsSync(mwDir)) {
|
|
149
|
+
fs.readdirSync(mwDir).forEach(file => {
|
|
150
|
+
if (file.endsWith('.js')) {
|
|
151
|
+
const name = file.replace('.js', '');
|
|
152
|
+
server.registerMiddleware(name, require(path.join(mwDir, file)));
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (server.namedMiddlewares['auth']) server.namedMiddlewares['AuthGuard'] = server.namedMiddlewares['auth'];
|
|
157
|
+
\n`;
|
|
158
|
+
|
|
159
|
+
ast.forEach(node => {
|
|
160
|
+
if (node.type === 'route') {
|
|
161
|
+
const v = node.view || 'home';
|
|
162
|
+
const viewName = v.charAt(0).toUpperCase() + v.slice(1);
|
|
163
|
+
const middlewares = node.middleware ? [node.middleware] : (node.middlewares || []);
|
|
164
|
+
let handlerCode = "";
|
|
165
|
+
if (node.handler && typeof node.handler === 'object' && node.handler.type === 'action_ref') {
|
|
166
|
+
handlerCode = actions[node.handler.name] || `console.error("Action not found: ${node.handler.name}");`;
|
|
167
|
+
} else {
|
|
168
|
+
handlerCode = node.handler || "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
output += `\nserver.route("${node.method.toLowerCase()}", "${node.path}", ${JSON.stringify(middlewares)}, async (req, res) => {
|
|
172
|
+
const user = (res.context && res.context.user) ? res.context.user : null;
|
|
173
|
+
let body = {};
|
|
174
|
+
try {
|
|
175
|
+
if (req.headers['content-type'] && req.headers['content-type'].includes('application/json')) {
|
|
176
|
+
body = await req.json().catch(() => ({}));
|
|
177
|
+
} else {
|
|
178
|
+
body = await req.urlencoded().catch(() => ({}));
|
|
179
|
+
}
|
|
180
|
+
} catch(e) {}
|
|
181
|
+
|
|
182
|
+
const result = await (async () => {\n` +
|
|
183
|
+
" " + handlerCode.replace(/res\.locals/g, 'res.context') + "\n" +
|
|
184
|
+
" })();\n\n" +
|
|
185
|
+
" if (res.headersSent) return;\n" +
|
|
186
|
+
"\n" +
|
|
187
|
+
" // If it's a PAGE route (has a view), render HTML\n" +
|
|
188
|
+
" if (" + (node.view ? 'true' : 'false') + ") {\n" +
|
|
189
|
+
" const helpers = {\n" +
|
|
190
|
+
" renderComponent: (name, props = {}) => {\n" +
|
|
191
|
+
" server.renderRegistry = server.renderRegistry || {\n" +
|
|
192
|
+
" " + Object.keys(components).map(name => `${name}: render${name}`).join(',\n ') + "\n" +
|
|
193
|
+
" };\n" +
|
|
194
|
+
" return server.renderRegistry[name] ? server.renderRegistry[name](props, helpers) : '<!-- Component ' + name + ' not found -->';\n" +
|
|
195
|
+
" }\n" +
|
|
196
|
+
" };\n" +
|
|
197
|
+
" const props = result || (res.context && res.context.props ? res.context.props : {});\n" +
|
|
198
|
+
" const pageHtml = helpers.renderComponent('" + viewName + "', props);\n" +
|
|
199
|
+
"\n" +
|
|
200
|
+
" if (req.headers['x-free-partial']) {\n" +
|
|
201
|
+
" return res.json({ title: 'Free Ultra | " + viewName + "', content: pageHtml, url: req.url });\n" +
|
|
202
|
+
" }\n" +
|
|
203
|
+
"\n" +
|
|
204
|
+
" const headerHtml = helpers.renderComponent('Header', props);\n" +
|
|
205
|
+
" const fullHTML = `<!DOCTYPE html><html lang=\"en\"><head>\n" +
|
|
206
|
+
" <meta charset=\"UTF-8\"><title>Free Ultra | " + viewName + "</title>\n" +
|
|
207
|
+
" <meta name=\"csrf-token\" content=\"${res.context.csrfToken || ''}\">\n" +
|
|
208
|
+
" <link href=\"https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;700;900&display=swap\" rel=\"stylesheet\">\n" +
|
|
209
|
+
" <link rel=\"stylesheet\" href=\"/css/app.css\">\n" +
|
|
210
|
+
" <style>\n" +
|
|
211
|
+
" :root { --primary:#00ff88; --secondary:#00bdff; --dark:#050505; }\n" +
|
|
212
|
+
" * { box-sizing:border-box; }\n" +
|
|
213
|
+
" body { margin:0; font-family:'Outfit',sans-serif; background:var(--dark); color:white; overflow-x:hidden; scroll-behavior:smooth; }\n" +
|
|
214
|
+
" main { width:100%; max-width:1000px; margin:0 auto; padding:6rem 2rem 2rem; min-height:100vh; }\n" +
|
|
215
|
+
" ${scopedCSS}\n" +
|
|
216
|
+
" </style>\n" +
|
|
217
|
+
" <script src=\"/free-runtime.js\" defer></script></head>\n" +
|
|
218
|
+
" <body>${headerHtml}<main id=\"free-app-root\">${pageHtml}</main></body></html>`;\n" +
|
|
219
|
+
" const minified = await minify(fullHTML, { collapseWhitespace: true, removeComments: true, continueOnParseError: true });\n" +
|
|
220
|
+
" res.header('Content-Type', 'text/html').send(fullHTML);\n" +
|
|
221
|
+
" } else {\n" +
|
|
222
|
+
" // API route - return JSON\n" +
|
|
223
|
+
" res.json({ success: true, result });\n" +
|
|
224
|
+
" }\n" +
|
|
225
|
+
"});\n";
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
output += `\nif (require.main === module) {
|
|
230
|
+
ORM.migrate(modelsRegistry).then(() => {
|
|
231
|
+
server.start(process.env.PORT || 4000);
|
|
232
|
+
}).catch(err => console.error(err));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = { ...modelsRegistry, server, modelsRegistry, ORM };
|
|
236
|
+
`;
|
|
237
|
+
|
|
238
|
+
return output;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function generateUltraComponent(node) {
|
|
242
|
+
const onMount = node.body.find(n => n.type === 'onMount')?.code || '';
|
|
243
|
+
const onDestroy = node.body.find(n => n.type === 'onDestroy')?.code || '';
|
|
244
|
+
const isInteractive = node.states.length > 0 || onMount !== '' || onDestroy !== '' || hasInteractivity(node.body);
|
|
245
|
+
const stateNames = node.states.map(s => s.name);
|
|
246
|
+
|
|
247
|
+
let code = `\nfunction render${node.name}(props = { }, helpers = { }) {\n`;
|
|
248
|
+
code += ` const e = (str) => String(str).replace(/[&<>'"]/g, m => ({'&': '&', '<': '<', '>': '>', "'": ''', '"': '"' }[m]));\n`;
|
|
249
|
+
code += ` const state = {\n`;
|
|
250
|
+
node.states.forEach(s => {
|
|
251
|
+
let val = s.default;
|
|
252
|
+
const isExpression = typeof val === 'string' && (val.includes('props.') || val.includes('||') || val.includes('&&') || val === 'true' || val === 'false' || val === 'null' || val.trim().startsWith('{') || val.trim().startsWith('['));
|
|
253
|
+
const finalVal = (isExpression || typeof val === 'number') ? val : `"${val}"`;
|
|
254
|
+
code += ` "${s.name}": ${finalVal},\n`;
|
|
255
|
+
});
|
|
256
|
+
code += ` };\n`;
|
|
257
|
+
if (stateNames.length > 0) {
|
|
258
|
+
code += ` const { ${stateNames.join(', ')} } = state;\n`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
code += ` const _events = [];\n`;
|
|
262
|
+
code += ` const islandAttr = ${isInteractive} ? ' data-component="${node.name}"' : '';\n`;
|
|
263
|
+
code += ` let html = "<div class='free-component'" + islandAttr + " data-state='" + JSON.stringify(state).replace(/'/g, "'") + "'";\n`;
|
|
264
|
+
|
|
265
|
+
if (onMount) {
|
|
266
|
+
code += ` _events.push({ id: 'onMount', fn: function(state) { ${onMount} } });\n`;
|
|
267
|
+
code += ` html += " data-free-on-mount='onMount'";\n`;
|
|
268
|
+
}
|
|
269
|
+
if (onDestroy) {
|
|
270
|
+
code += ` _events.push({ id: 'onDestroy', fn: function(state) { ${onDestroy} } });\n`;
|
|
271
|
+
code += ` html += " data-free-on-destroy='onDestroy'";\n`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
code += ` html += ">";\n`;
|
|
275
|
+
code += generateComponentBody(node.body, stateNames);
|
|
276
|
+
|
|
277
|
+
if (isInteractive) {
|
|
278
|
+
code += ` html += "<script>window.__free_actions = window.__free_actions || {}; window.__free_actions['${node.name}'] = {};";\n`;
|
|
279
|
+
code += ` _events.forEach(ev => {\n`;
|
|
280
|
+
code += ` html += "window.__free_actions['${node.name}'][ev.id] = " + ev.fn.toString() + ";";\n`;
|
|
281
|
+
code += ` });\n`;
|
|
282
|
+
code += ` html += "</script>";\n`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
code += ` html += "</div>";\n return html;\n}\n`;
|
|
286
|
+
return code;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function generateComponentBody(body, stateNames) {
|
|
290
|
+
let code = "";
|
|
291
|
+
body.forEach(node => {
|
|
292
|
+
if (node.type === 'tag') {
|
|
293
|
+
code += generateTagCode(node, stateNames);
|
|
294
|
+
} else if (node.type === 'event') {
|
|
295
|
+
return;
|
|
296
|
+
} else if (node.type === 'script') {
|
|
297
|
+
const scriptCode = node.code.replace(/`/g, '\\`').replace(/\$/g, '\\$');
|
|
298
|
+
code += ` html += \`<script>${scriptCode}</script>\`;\n`;
|
|
299
|
+
} else if (node.type === 'loop') {
|
|
300
|
+
code += " if (Array.isArray(" + node.list + ")) {\n";
|
|
301
|
+
code += " " + node.list + ".forEach(" + node.item + " => {\n";
|
|
302
|
+
code += generateComponentBody(node.body, stateNames).replace(/^ /gm, ' ');
|
|
303
|
+
code += " });\n";
|
|
304
|
+
code += " }\n";
|
|
305
|
+
} else if (node.type === 'condition') {
|
|
306
|
+
code += " if (" + node.condition + ") {\n";
|
|
307
|
+
code += generateComponentBody(node.body, stateNames).replace(/^ /gm, ' ');
|
|
308
|
+
code += " }\n";
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
return code;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function hasInteractivity(nodes) {
|
|
315
|
+
return nodes.some(n => {
|
|
316
|
+
if (n.type === 'tag') {
|
|
317
|
+
if (n.attributes && Object.keys(n.attributes).some(a => a.startsWith('on'))) return true;
|
|
318
|
+
if (n.children && hasInteractivity(n.children)) return true;
|
|
319
|
+
}
|
|
320
|
+
return false;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function generateTagCode(tag, stateNames) {
|
|
325
|
+
const isComponent = tag.name[0] === tag.name[0].toUpperCase();
|
|
326
|
+
if (isComponent) {
|
|
327
|
+
let propsStr = "{";
|
|
328
|
+
if (tag.attributes) {
|
|
329
|
+
Object.keys(tag.attributes).forEach((a, i) => {
|
|
330
|
+
let val = tag.attributes[a].replace(/\{([^}]+)\}/g, (_, p) => "${" + p + "}");
|
|
331
|
+
propsStr += (i === 0 ? "" : ", ") + '"' + a + '": `' + val + '`';
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
propsStr += "}";
|
|
335
|
+
return ` html += helpers.renderComponent('${tag.name}', ${propsStr}, helpers);\n`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
let code = ` html += "<${tag.name}";\n`;
|
|
339
|
+
|
|
340
|
+
if (tag.attributes) {
|
|
341
|
+
Object.keys(tag.attributes).forEach(a => {
|
|
342
|
+
if (a.startsWith('on')) {
|
|
343
|
+
const eventName = a.substring(2).toLowerCase();
|
|
344
|
+
const uniqueId = `\${eventName}_\${tag.name}_\${Math.random().toString(36).substring(7)}`;
|
|
345
|
+
if (tag.attributes[a].includes('++')) {
|
|
346
|
+
const key = tag.attributes[a].split('++')[0].trim();
|
|
347
|
+
code += ` _events.push({ id: '${uniqueId}', fn: function(state) { state['${key}']++; } });\n`;
|
|
348
|
+
} else {
|
|
349
|
+
code += ` _events.push({ id: '${uniqueId}', fn: function(state, event) { ${tag.attributes[a]} } });\n`;
|
|
350
|
+
}
|
|
351
|
+
code += ` html += " data-on-${eventName}='${uniqueId}'";\n`;
|
|
352
|
+
} else {
|
|
353
|
+
let val = tag.attributes[a].replace(/\{([^}]+)\}/g, (_, p) => "${e(" + p + ")}");
|
|
354
|
+
code += " html += \" \" + " + JSON.stringify(a) + " + \"='\" + `" + val + "` + \"'\";\n";
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (tag.children) {
|
|
360
|
+
tag.children.forEach(child => {
|
|
361
|
+
if (child.type === 'event') {
|
|
362
|
+
const eventName = child.event.toLowerCase();
|
|
363
|
+
const uniqueId = `\${eventName}_child_\${Math.random().toString(36).substring(7)}`;
|
|
364
|
+
code += ` _events.push({ id: '${uniqueId}', fn: function(state, event) { ${child.code} } });\n`;
|
|
365
|
+
code += ` html += " data-on-${eventName}='${uniqueId}'";\n`;
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
code += ` html += ">";\n`;
|
|
371
|
+
|
|
372
|
+
if (tag.content) {
|
|
373
|
+
let cont = tag.content.replace(/`/g, '\\`').replace(/\$/g, '\\$').replace(/\{([^}]+)\}/g, (_, p) => {
|
|
374
|
+
const parts = p.split('.');
|
|
375
|
+
if (stateNames.includes(parts[0])) return `\${e(state["${parts[0]}"]${parts.slice(1).map(part => `?.["${part}"]`).join('')})}`;
|
|
376
|
+
return `\${e(${p})}`;
|
|
377
|
+
});
|
|
378
|
+
code += ` html += \`${cont}\`;\n`;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (tag.children) code += generateComponentBody(tag.children, stateNames);
|
|
382
|
+
code += ` html += "</${tag.name}>";\n`;
|
|
383
|
+
return code;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = { generate };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* compiler/lexer.js
|
|
3
|
+
* Lexer for the Free Framework.
|
|
4
|
+
* Converts .free code into tokens WITH line & column tracking.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class FreeSyntaxError extends SyntaxError {
|
|
8
|
+
constructor(message, file, line, col) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'FreeSyntaxError';
|
|
11
|
+
this.file = file || 'unknown.free';
|
|
12
|
+
this.line = line;
|
|
13
|
+
this.col = col;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
toString() {
|
|
17
|
+
return `\n\x1b[31mFreeSyntaxError\x1b[0m in \x1b[33m${this.file}\x1b[0m\n` +
|
|
18
|
+
` Line ${this.line}, Col ${this.col}: ${this.message}\n`;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const KEYWORDS = new Set([
|
|
23
|
+
'route', 'model', 'component', 'page', 'view', 'link', 'state', 'on', 'style',
|
|
24
|
+
'middleware', 'auth', 'login', 'register', 'upload', 'path', 'maxSize', 'error',
|
|
25
|
+
'text', 'action', 'validate', 'store', 'onMount', 'onDestroy', 'queue', 'mail',
|
|
26
|
+
'loop', 'condition', 'script', 'if', 'for'
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
function tokenize(code, filename = 'unknown.free') {
|
|
30
|
+
const tokens = [];
|
|
31
|
+
let i = 0;
|
|
32
|
+
let line = 1;
|
|
33
|
+
let col = 1;
|
|
34
|
+
|
|
35
|
+
while (i < code.length) {
|
|
36
|
+
const ch = code[i];
|
|
37
|
+
|
|
38
|
+
// Skip whitespace, track position
|
|
39
|
+
if (ch === '\n') {
|
|
40
|
+
line++;
|
|
41
|
+
col = 1;
|
|
42
|
+
i++;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '\r' || ch === '\t' || ch === ' ') {
|
|
46
|
+
col++;
|
|
47
|
+
i++;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Single-line comments // ...
|
|
52
|
+
if (ch === '/' && code[i + 1] === '/') {
|
|
53
|
+
while (i < code.length && code[i] !== '\n') i++;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Multi-line block comments /* ... */
|
|
58
|
+
if (ch === '/' && code[i + 1] === '*') {
|
|
59
|
+
i += 2; // skip /*
|
|
60
|
+
while (i < code.length && !(code[i] === '*' && code[i + 1] === '/')) {
|
|
61
|
+
if (code[i] === '\n') {
|
|
62
|
+
line++;
|
|
63
|
+
col = 1;
|
|
64
|
+
} else {
|
|
65
|
+
col++;
|
|
66
|
+
}
|
|
67
|
+
i++;
|
|
68
|
+
}
|
|
69
|
+
i += 2; // skip */
|
|
70
|
+
col += 2;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Strings "...", '...', `...`
|
|
75
|
+
if (ch === '"' || ch === "'" || ch === '`') {
|
|
76
|
+
const quoteStart = ch;
|
|
77
|
+
const startLine = line;
|
|
78
|
+
const startCol = col;
|
|
79
|
+
const startIdx = i;
|
|
80
|
+
let str = '';
|
|
81
|
+
i++; col++;
|
|
82
|
+
while (i < code.length && code[i] !== quoteStart) {
|
|
83
|
+
// Handle escapes
|
|
84
|
+
if (code[i] === '\\') {
|
|
85
|
+
str += code[i];
|
|
86
|
+
i++; col++;
|
|
87
|
+
str += code[i];
|
|
88
|
+
i++; col++;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (code[i] === '\n') {
|
|
93
|
+
if (quoteStart !== '`') {
|
|
94
|
+
throw new FreeSyntaxError('Unterminated string literal', filename, startLine, startCol);
|
|
95
|
+
}
|
|
96
|
+
line++;
|
|
97
|
+
col = 1;
|
|
98
|
+
} else {
|
|
99
|
+
col++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
str += code[i];
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
if (i >= code.length) {
|
|
106
|
+
throw new FreeSyntaxError('Unterminated string literal', filename, startLine, startCol);
|
|
107
|
+
}
|
|
108
|
+
i++; col++; // closing quote
|
|
109
|
+
tokens.push({ type: 'STRING', value: `${quoteStart}${str}${quoteStart}`, raw: str, line: startLine, col: startCol, start: startIdx, end: i });
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Braces
|
|
114
|
+
if (ch === '{' || ch === '}') {
|
|
115
|
+
tokens.push({ type: 'BRACE', value: ch, line, col, start: i, end: i + 1 });
|
|
116
|
+
i++; col++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Parens
|
|
121
|
+
if (ch === '(' || ch === ')') {
|
|
122
|
+
tokens.push({ type: 'PAREN', value: ch, line, col, start: i, end: i + 1 });
|
|
123
|
+
i++; col++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Numbers
|
|
128
|
+
if (ch >= '0' && ch <= '9') {
|
|
129
|
+
const startCol = col;
|
|
130
|
+
const startLine = line;
|
|
131
|
+
const startIdx = i;
|
|
132
|
+
let num = '';
|
|
133
|
+
while (i < code.length && code[i] >= '0' && code[i] <= '9') {
|
|
134
|
+
num += code[i];
|
|
135
|
+
i++; col++;
|
|
136
|
+
}
|
|
137
|
+
tokens.push({ type: 'NUMBER', value: num, line: startLine, col: startCol, start: startIdx, end: i });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Identifiers and Keywords
|
|
142
|
+
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_') {
|
|
143
|
+
const startCol = col;
|
|
144
|
+
const startLine = line;
|
|
145
|
+
const startIdx = i;
|
|
146
|
+
let word = '';
|
|
147
|
+
while (i < code.length && /[\w\-]/.test(code[i])) {
|
|
148
|
+
word += code[i];
|
|
149
|
+
i++; col++;
|
|
150
|
+
}
|
|
151
|
+
const type = KEYWORDS.has(word) ? 'KEYWORD' : 'IDENTIFIER';
|
|
152
|
+
tokens.push({ type, value: word, line: startLine, col: startCol, start: startIdx, end: i });
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Symbols (operators, punctuation)
|
|
157
|
+
const symStart = col;
|
|
158
|
+
const startIdx = i;
|
|
159
|
+
tokens.push({ type: 'SYMBOL', value: ch, line, col: symStart, start: startIdx, end: i + 1 });
|
|
160
|
+
i++; col++;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return tokens;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { tokenize, FreeSyntaxError, KEYWORDS };
|