veko-framework 1.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/src/cli/dev.ts ADDED
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Veko v1 – Dev server with hot restart
4
+ * Watches .vk files, recompiles on change, restarts the generated server.
5
+ */
6
+
7
+ import path from 'path';
8
+ import { fileURLToPath } from 'url';
9
+ import { spawn, ChildProcess } from 'child_process';
10
+ import chokidar from 'chokidar';
11
+ import chalk from 'chalk';
12
+ import { DEFAULTS } from '../config/defaults.js';
13
+
14
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
+ /** Project root: where the user ran veko (process.cwd()), so dev/build work in the consumer app. */
16
+ const ROOT = process.cwd();
17
+
18
+ const SIGKILL_TIMEOUT_MS = 5000;
19
+ const REBUILD_DEBOUNCE_MS = 300;
20
+
21
+ interface CompileResult {
22
+ config: { output: { server: string }; server?: { port?: number } };
23
+ output: { server: string; types: string };
24
+ }
25
+
26
+ let serverProcess: ChildProcess | null = null;
27
+ let serverPath: string | null = null;
28
+
29
+ function log(msg: string, style: 'info' | 'success' | 'error' | 'warn' = 'info') {
30
+ const prefix = chalk.gray('[veko]');
31
+ switch (style) {
32
+ case 'success':
33
+ console.log(prefix, chalk.green(msg));
34
+ break;
35
+ case 'error':
36
+ console.error(prefix, chalk.red(msg));
37
+ break;
38
+ case 'warn':
39
+ console.warn(prefix, chalk.yellow(msg));
40
+ break;
41
+ default:
42
+ console.log(prefix, msg);
43
+ }
44
+ }
45
+
46
+ async function runCompile(): Promise<CompileResult | null> {
47
+ const { compile } = await import('../compiler/compile.js');
48
+ try {
49
+ const result = (await compile(ROOT)) as CompileResult;
50
+ return result;
51
+ } catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ log(`Compilation failed: ${message}`, 'error');
54
+ if (err instanceof Error && err.stack) {
55
+ console.error(chalk.gray(err.stack));
56
+ }
57
+ return null;
58
+ }
59
+ }
60
+
61
+ function startServer(serverPath: string, port: number): ChildProcess {
62
+ const child = spawn('node', [serverPath], {
63
+ cwd: ROOT,
64
+ stdio: 'inherit',
65
+ env: { ...process.env, PORT: String(port), NODE_ENV: 'development' },
66
+ });
67
+ child.on('error', (err) => {
68
+ log(`Server process error: ${err.message}`, 'error');
69
+ });
70
+ child.on('exit', (code, signal) => {
71
+ if (code !== null && code !== 0 && signal !== 'SIGTERM' && signal !== 'SIGKILL') {
72
+ log(`Server exited with code ${code}`, 'warn');
73
+ }
74
+ });
75
+ return child;
76
+ }
77
+
78
+ function killServer(): Promise<void> {
79
+ return new Promise((resolve) => {
80
+ if (!serverProcess) {
81
+ resolve();
82
+ return;
83
+ }
84
+ const proc = serverProcess;
85
+ serverProcess = null;
86
+
87
+ const done = () => {
88
+ clearTimeout(killTimeout);
89
+ proc.removeAllListeners('exit');
90
+ resolve();
91
+ };
92
+
93
+ proc.once('exit', () => done());
94
+
95
+ proc.kill('SIGTERM');
96
+
97
+ const killTimeout = setTimeout(() => {
98
+ try {
99
+ proc.kill('SIGKILL');
100
+ } catch {
101
+ /* already gone */
102
+ }
103
+ done();
104
+ }, SIGKILL_TIMEOUT_MS);
105
+ });
106
+ }
107
+
108
+ let rebuildDebounceTimer: ReturnType<typeof setTimeout> | null = null;
109
+ let pendingChangedFile: string | undefined;
110
+
111
+ async function rebuildAndRestart(changedFile?: string): Promise<void> {
112
+ pendingChangedFile = changedFile ?? pendingChangedFile;
113
+
114
+ if (rebuildDebounceTimer) {
115
+ clearTimeout(rebuildDebounceTimer);
116
+ }
117
+ rebuildDebounceTimer = setTimeout(async () => {
118
+ rebuildDebounceTimer = null;
119
+ const fileToLog = pendingChangedFile;
120
+ pendingChangedFile = undefined;
121
+
122
+ const result = await runCompile();
123
+ if (!result) {
124
+ if (fileToLog) {
125
+ log(`Keeping previous server running. Fix errors and save again.`, 'warn');
126
+ }
127
+ return;
128
+ }
129
+
130
+ const { output, config } = result;
131
+ const port = config.server?.port ?? DEFAULTS.PORT;
132
+
133
+ await killServer();
134
+
135
+ serverPath = output.server;
136
+ serverProcess = startServer(output.server, port);
137
+
138
+ if (fileToLog) {
139
+ const relative = path.relative(ROOT, fileToLog);
140
+ log(chalk.bold(`🚀 Server reloaded due to changes in ${relative}`), 'success');
141
+ }
142
+ }, REBUILD_DEBOUNCE_MS);
143
+ }
144
+
145
+ function main(): void {
146
+ log(chalk.cyan('Starting Veko v1 dev server...'));
147
+
148
+ // Watch only .vk in project root (and entry dir if different) to avoid EMFILE
149
+ const glob = path.join(ROOT, '**/*.vk');
150
+ const watcher = chokidar.watch(glob, {
151
+ ignoreInitial: true,
152
+ awaitWriteFinish: { stabilityThreshold: 150 },
153
+ ignored: [/(^|[/\\])node_modules([/\\]|$)/, /([/\\])\.git([/\\]|$)/],
154
+ });
155
+
156
+ watcher.on('change', (filePath) => {
157
+ log(`Change detected: ${path.relative(ROOT, filePath)}`);
158
+ rebuildAndRestart(filePath);
159
+ });
160
+
161
+ watcher.on('add', (filePath) => {
162
+ log(`New .vk file: ${path.relative(ROOT, filePath)}`);
163
+ rebuildAndRestart(filePath);
164
+ });
165
+
166
+ watcher.on('ready', () => {
167
+ log('Watching .vk files...');
168
+ rebuildAndRestart();
169
+ });
170
+
171
+ watcher.on('error', (err: unknown) => {
172
+ log(`Watcher error: ${err instanceof Error ? err.message : String(err)}`, 'error');
173
+ });
174
+
175
+ process.on('SIGINT', async () => {
176
+ log('Shutting down...');
177
+ await killServer();
178
+ watcher.close();
179
+ process.exit(0);
180
+ });
181
+
182
+ process.on('SIGTERM', async () => {
183
+ await killServer();
184
+ watcher.close();
185
+ process.exit(0);
186
+ });
187
+ }
188
+
189
+ main();
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Veko v1 – Migrate (apply versioned migrations from .vk or migrations dir)
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { fileURLToPath, pathToFileURL } from 'url';
9
+ import { DEFAULTS } from '../config/defaults.js';
10
+
11
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
12
+ const root = process.cwd();
13
+
14
+ async function loadConfig() {
15
+ const configPath = path.join(root, 'veko.config.js');
16
+ let adapter = 'sqlite';
17
+ let dbPath = path.join(root, process.env.DB_FILE || (process.env.NODE_ENV === 'test' ? DEFAULTS.DB_FILE_TEST : DEFAULTS.DB_FILE));
18
+ let migrationsTable = '_veko_migrations';
19
+ if (fs.existsSync(configPath)) {
20
+ const mod = await import(pathToFileURL(configPath).href);
21
+ const c = mod.default || mod;
22
+ adapter = c.adapter || 'sqlite';
23
+ dbPath = c.database?.sqlite?.path || dbPath;
24
+ migrationsTable = c.migrations?.table || migrationsTable;
25
+ }
26
+ return { adapter, dbPath, migrationsTable };
27
+ }
28
+
29
+ function ensureMigrationsTable(db, table) {
30
+ db.exec(`
31
+ CREATE TABLE IF NOT EXISTS "${table}" (
32
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
33
+ version TEXT UNIQUE NOT NULL,
34
+ applied_at TEXT DEFAULT (datetime('now'))
35
+ )
36
+ `);
37
+ }
38
+
39
+ function getAppliedVersions(db, table) {
40
+ const rows = db.prepare(`SELECT version FROM "${table}" ORDER BY id`).all();
41
+ return new Set(rows.map((r) => r.version));
42
+ }
43
+
44
+ async function main() {
45
+ const { adapter, dbPath, migrationsTable } = await loadConfig();
46
+ if (adapter !== 'sqlite') {
47
+ console.log('Only SQLite migrations are implemented in this CLI. Use Postgres manually or extend this script.');
48
+ process.exit(0);
49
+ }
50
+ const Database = (await import('better-sqlite3')).default;
51
+ const db = new Database(dbPath);
52
+ ensureMigrationsTable(db, migrationsTable);
53
+ const applied = getAppliedVersions(db, migrationsTable);
54
+
55
+ // Load migrations from api.vk (parser) or from migrations/*.sql / migrations/*.vk
56
+ const migrationsDir = path.join(root, 'migrations');
57
+ const migrations = [];
58
+ if (fs.existsSync(migrationsDir)) {
59
+ const files = fs.readdirSync(migrationsDir).filter((f) => f.endsWith('.sql'));
60
+ for (const f of files.sort()) {
61
+ const version = f.replace(/\.sql$/, '');
62
+ if (applied.has(version)) continue;
63
+ const sql = fs.readFileSync(path.join(migrationsDir, f), 'utf-8');
64
+ migrations.push({ version, sql });
65
+ }
66
+ }
67
+
68
+ for (const { version, sql } of migrations) {
69
+ console.log('Applying', version);
70
+ db.exec(sql);
71
+ db.prepare(`INSERT INTO "${migrationsTable}" (version) VALUES (?)`).run(version);
72
+ }
73
+ if (migrations.length === 0) console.log('No new migrations.');
74
+ db.close();
75
+ }
76
+
77
+ main().catch((err) => {
78
+ console.error(err);
79
+ process.exit(1);
80
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Veko v1 – AOT validation code generation
3
+ * Produces inline validate_SchemaName(data) functions and TypeScript field types.
4
+ */
5
+
6
+ /**
7
+ * Generate the validate_SchemaName(data) function source for a data block.
8
+ * @param {{ name: string, fields: Array<{ name: string, type: string, optional?: boolean, args?: { min?: number, max?: number, enum?: string[] } }> }} schema
9
+ * @returns {string}
10
+ */
11
+ export function schemaToValidationCode(schema) {
12
+ let code = `function validate_${schema.name}(data) {\n`;
13
+ code += ` if (data === null || typeof data !== 'object' || Array.isArray(data)) {\n`;
14
+ code += ` throw Object.assign(new Error('${schema.name}: body must be a plain object'), { status: 400 });\n`;
15
+ code += ` }\n`;
16
+ code += ` const result = {};\n`;
17
+ for (const f of schema.fields) {
18
+ const key = f.name;
19
+ const req = !f.optional;
20
+ code += ` if (data['${key}'] === undefined || data['${key}'] === null) {\n`;
21
+ if (req) {
22
+ code += ` throw Object.assign(new Error('${schema.name}: missing required field "${key}"'), { status: 400 });\n`;
23
+ } else {
24
+ code += ` result['${key}'] = null;\n`;
25
+ }
26
+ code += ` } else {\n`;
27
+
28
+ if (['string', 'password', 'date', 'enum'].includes(f.type.toLowerCase()) || f.args?.enum) {
29
+ code += ` if (typeof data['${key}'] !== 'string') throw Object.assign(new Error('${schema.name}: "${key}" must be a string'), { status: 400 });\n`;
30
+ if (f.args?.min !== undefined) code += ` if (data['${key}'].length < ${f.args.min}) throw Object.assign(new Error('${schema.name}: "${key}" too short'), { status: 400 });\n`;
31
+ if (f.args?.max !== undefined) code += ` if (data['${key}'].length > ${f.args.max}) throw Object.assign(new Error('${schema.name}: "${key}" too long'), { status: 400 });\n`;
32
+ if (f.args?.enum) {
33
+ const enumStr = JSON.stringify(f.args.enum);
34
+ code += ` if (!${enumStr}.includes(data['${key}'])) throw Object.assign(new Error('${schema.name}: "${key}" invalid enum'), { status: 400 });\n`;
35
+ }
36
+ code += ` result['${key}'] = data['${key}'];\n`;
37
+ } else if (f.type.toLowerCase() === 'number') {
38
+ code += ` const n = Number(data['${key}']);\n`;
39
+ code += ` if (!Number.isFinite(n)) throw Object.assign(new Error('${schema.name}: "${key}" must be a number'), { status: 400 });\n`;
40
+ if (f.args?.min !== undefined) code += ` if (n < ${f.args.min}) throw Object.assign(new Error('${schema.name}: "${key}" must be >= ${f.args.min}'), { status: 400 });\n`;
41
+ if (f.args?.max !== undefined) code += ` if (n > ${f.args.max}) throw Object.assign(new Error('${schema.name}: "${key}" must be <= ${f.args.max}'), { status: 400 });\n`;
42
+ code += ` result['${key}'] = n;\n`;
43
+ } else if (f.type.toLowerCase() === 'boolean') {
44
+ code += ` result['${key}'] = Boolean(data['${key}']);\n`;
45
+ } else {
46
+ code += ` result['${key}'] = data['${key}'];\n`;
47
+ }
48
+ code += ` }\n`;
49
+ }
50
+ code += ` return result;\n`;
51
+ code += `}\n\n`;
52
+ return code;
53
+ }
54
+
55
+ /**
56
+ * @param {{ type: string }} f
57
+ * @returns {string}
58
+ */
59
+ export function fieldToTsType(f) {
60
+ if (f.type === 'Number') return 'number';
61
+ if (f.type === 'Boolean') return 'boolean';
62
+ if (f.type === 'String' || f.type === 'Enum' || f.type === 'Password') return 'string';
63
+ if (f.type === 'Date') return 'string';
64
+ return 'unknown';
65
+ }
66
+
67
+ export default { schemaToValidationCode, fieldToTsType };
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Veko v1 – AOT-Optimized Code Generation
3
+ * Lean and modular: uses aot-validation, QueryBuilder, wrapAction, shared defaults.
4
+ */
5
+
6
+ import { schemaToValidationCode, fieldToTsType } from './aot-validation.js';
7
+ import { DEFAULTS } from '../config/defaults.js';
8
+
9
+ export function generate(ast, options = {}) {
10
+ const { adapter = 'sqlite', serverPath = './generated/server.js', runtimeDir = './runtime' } = options;
11
+ const relRuntime = runtimeDir.startsWith('.') ? runtimeDir : `./${runtimeDir}`;
12
+
13
+ const knownTables = new Set(ast.dataBlocks.map((b) => b.name));
14
+
15
+ const adapterImport = adapter === 'postgres' ? 'PostgresAdapter' : 'SqliteAdapter';
16
+ const adapterPath = `${relRuntime}/adapters/${adapter}.js`;
17
+
18
+ let server = `/**
19
+ * Generated by Veko v1 – AOT Optimized Server (Lean & Modular)
20
+ */
21
+
22
+ import express from 'express';
23
+ import jwt from 'jsonwebtoken';
24
+ import { signToken } from '${relRuntime}/auth.js';
25
+ import { wrapAction } from '${relRuntime}/wrapAction.js';
26
+ import { toVekoError } from '${relRuntime}/errors.js';
27
+ import QueryBuilder from '${relRuntime}/queryBuilder.js';
28
+ import { ${adapterImport} } from '${adapterPath}';
29
+
30
+ const app = express();
31
+ app.use(express.json());
32
+
33
+ // Database adapter connection
34
+ `;
35
+ if (adapter === 'sqlite') {
36
+ server += `const db = new SqliteAdapter(process.env.DB_FILE || '${DEFAULTS.DB_FILE}');\n`;
37
+ } else {
38
+ server += `const db = new PostgresAdapter(process.env.DATABASE_URL);\n`;
39
+ }
40
+ server += `await db.connect();\n\n`;
41
+
42
+ const tableCols = {};
43
+ for (const b of ast.dataBlocks) tableCols[b.name] = b.fields.map((f) => f.name);
44
+ server += `const TABLE_COLS = ${JSON.stringify(tableCols)};\n`;
45
+ server += `const PREP = QueryBuilder.prepareAll('${adapter}', db, TABLE_COLS);\n\n`;
46
+ server += `\n// AOT Database Helpers\nconst aot_db = {\n`;
47
+ for (const b of ast.dataBlocks) {
48
+ const t = b.name;
49
+ const args = b.fields.map(f => `data.${f.name}`);
50
+ const allowedCols = JSON.stringify(['id', ...b.fields.map(f => f.name)]);
51
+ server += ` ${t}: {\n`;
52
+ if (adapter === 'sqlite') {
53
+ server += ` save: (data) => { const r = PREP['${t}'].insert.run(${args.join(', ')}); return { id: r.lastInsertRowid }; },\n`;
54
+ server += ` update: (id, data) => PREP['${t}'].update.run(${args.join(', ')}, id),\n`;
55
+ server += ` remove: (id) => PREP['${t}'].delete.run(id),\n`;
56
+ server += ` find: (id) => PREP['${t}'].findById.get(id),\n`;
57
+ server += ` findAll: async (where = {}, orderBy = null) => {
58
+ const allowed = ${allowedCols};
59
+ for (const k of Object.keys(where)) {
60
+ if (!allowed.includes(k)) throw Object.assign(new Error('Invalid where column: ' + k), { status: 400 });
61
+ }
62
+ if (orderBy && !allowed.includes(orderBy.field)) throw Object.assign(new Error('Invalid orderBy column: ' + orderBy.field), { status: 400 });
63
+ return db.findAll('${t}', where, orderBy);
64
+ }\n`;
65
+ } else {
66
+ server += ` save: async (data) => { const r = await db.pool.query(PREP['${t}'].insert, [${args.join(', ')}]); return { id: r.rows[0].id }; },\n`;
67
+ server += ` update: async (id, data) => await db.pool.query(PREP['${t}'].update, [${args.join(', ')}, id]),\n`;
68
+ server += ` remove: async (id) => await db.pool.query(PREP['${t}'].delete, [id]),\n`;
69
+ server += ` find: async (id) => { const r = await db.pool.query(PREP['${t}'].findById, [id]); return r.rows[0]; },\n`;
70
+ server += ` findAll: async (where = {}, orderBy = null) => {
71
+ const allowed = ${allowedCols};
72
+ for (const k of Object.keys(where)) {
73
+ if (!allowed.includes(k)) throw Object.assign(new Error('Invalid where column: ' + k), { status: 400 });
74
+ }
75
+ if (orderBy && !allowed.includes(orderBy.field)) throw Object.assign(new Error('Invalid orderBy column: ' + orderBy.field), { status: 400 });
76
+ return db.findAll('${t}', where, orderBy);
77
+ }\n`;
78
+ }
79
+ server += ` },\n`;
80
+ }
81
+ server += ` query: async (sql, ...params) => {
82
+ const trimmed = (typeof sql === 'string' ? sql : '').trim();
83
+ if (!trimmed.toUpperCase().startsWith('SELECT')) {
84
+ throw Object.assign(new Error('sugar.query() is restricted to SELECT statements only.'), { status: 403 });
85
+ }
86
+ return db.query(sql, params);
87
+ }\n};\n\n`;
88
+
89
+ server += `// Static Validation Blocks\n`;
90
+ for (const b of ast.dataBlocks) {
91
+ server += schemaToValidationCode(b);
92
+ }
93
+
94
+ server += `\n// Fast-path JSON Stringifiers\n`;
95
+ for (const b of ast.dataBlocks) {
96
+ server += `function fastJson_${b.name}(data, _d) {\n`;
97
+ server += ` const depth = _d ?? 0;\n`;
98
+ server += ` if (depth > 10) return 'null';\n`;
99
+ server += ` if (Array.isArray(data)) return '[' + data.map((x) => fastJson_${b.name}(x, depth + 1)).join(',') + ']';\n`;
100
+ server += ` if (!data) return 'null';\n`;
101
+ let parts = [`'"id":' + data.id`];
102
+ for (const f of b.fields) {
103
+ if (['string', 'password', 'date', 'enum'].includes(f.type.toLowerCase()) || f.args?.enum) {
104
+ parts.push(`'"${f.name}":"' + (data.${f.name} || '').toString().replace(/(["\\\\\\n\\r\\t])/g, '\\\\$1') + '"'`);
105
+ } else {
106
+ parts.push(`'"${f.name}":' + (data.${f.name} !== undefined ? data.${f.name} : 'null')`);
107
+ }
108
+ }
109
+ server += ` return '{' + ${parts.join(" + ',' + ")} + '}';\n`;
110
+ server += `}\n\n`;
111
+ }
112
+
113
+ for (const block of ast.doBlocks) {
114
+ let body = block.body;
115
+ body = body.replace(/sugar\.save\(\s*['"]([^'"]+)['"]\s*,\s*(.*?)\)/g, "aot_db.$1.save($2)");
116
+ body = body.replace(/sugar\.find\(\s*['"]([^'"]+)['"]\s*,\s*(.*?)\)/g, "aot_db.$1.find($2)");
117
+ body = body.replace(/sugar\.remove\(\s*['"]([^'"]+)['"]\s*,\s*(.*?)\)/g, "aot_db.$1.remove($2)");
118
+ body = body.replace(/sugar\.update\(\s*['"]([^'"]+)['"]\s*,\s*(.*?)\s*,\s*(.*?)\)/g, "aot_db.$1.update($2, $3)");
119
+ body = body.replace(/sugar\.all\(\s*['"]([^'"]+)['"]\s*(?:,\s*([^)]*))?\)/g, (_, table, args) =>
120
+ args ? `aot_db.${table}.findAll(${args})` : `aot_db.${table}.findAll()`);
121
+ body = body.replace(/sugar\.query/g, "aot_db.query");
122
+
123
+ server += `async function ${block.name}(data) {\n`;
124
+ server += ` ${body.split('\n').join('\n ')}\n`;
125
+ server += `}\n\n`;
126
+ }
127
+
128
+ server += `app.get('/__health', (req, res) => res.json({ status: 'ok', engine: 'Veko v1 AOT' }));\n\n`;
129
+
130
+ for (const route of ast.routeLines) {
131
+ const method = route.method.toLowerCase();
132
+ let needsAuth = false;
133
+ let schemaToValidate = null;
134
+ let actionName = null;
135
+ for (const step of route.pipeline) {
136
+ if (step.kind === 'auth') needsAuth = true;
137
+ else if (step.kind === 'validate') schemaToValidate = step.schema;
138
+ else if (step.kind === 'action') actionName = step.name;
139
+ }
140
+ if (!actionName) continue;
141
+
142
+ server += `app.${method}("${route.path}", wrapAction(async (req, res) => {\n`;
143
+
144
+ if (needsAuth) {
145
+ server += ` const rawAuth = req.headers.authorization;\n`;
146
+ server += ` const header = Array.isArray(rawAuth) ? rawAuth[0] : rawAuth;\n`;
147
+ server += ` if (typeof header !== 'string' || !header.startsWith('Bearer ')) throw Object.assign(new Error('Unauthorized'), { status: 401 });\n`;
148
+ server += ` req.user = jwt.verify(header.slice(7), process.env.JWT_SECRET || '${DEFAULTS.JWT_SECRET}', { algorithms: ['HS256'] });\n`;
149
+ }
150
+
151
+ if (schemaToValidate) {
152
+ server += ` const validatedBody = validate_${schemaToValidate}(req.body || {});\n`;
153
+ server += ` const ctx = { ...req.params, ...validatedBody };\n`;
154
+ } else {
155
+ server += ` const ctx = { ...req.params, ...req.query, ...(req.body || {}) };\n`;
156
+ }
157
+
158
+ server += ` const result = await ${actionName}(ctx);\n`;
159
+
160
+ if (schemaToValidate) {
161
+ server += ` if (result && typeof result === 'object' && !result.token) {\n`;
162
+ server += ` res.type('json').send(fastJson_${schemaToValidate}(result));\n`;
163
+ server += ` } else {\n`;
164
+ server += ` res.json(result ?? { success: true });\n`;
165
+ server += ` }\n`;
166
+ } else {
167
+ server += ` res.json(result ?? { success: true });\n`;
168
+ }
169
+
170
+ server += `}));\n\n`;
171
+ }
172
+
173
+ server += `app.use((err, req, res, next) => {\n`;
174
+ server += ` if (res.headersSent) return next(err);\n`;
175
+ server += ` const e = toVekoError(err);\n`;
176
+ server += ` res.status(e.status).json({ error: e.message, message: e.message });\n`;
177
+ server += `});\n\n`;
178
+
179
+ server += `const port = parseInt(process.env.PORT || '${DEFAULTS.PORT}', 10);\n`;
180
+ server += `const host = process.env.HOST || '${DEFAULTS.HOST}';\n`;
181
+ server += `const server = app.listen(port, host, () => {\n`;
182
+ server += ` console.log('Veko v1 server on http://' + host + ':' + port);\n`;
183
+ server += `});\n`;
184
+ server += `server.on('error', (err) => {\n`;
185
+ server += ` if (err.code === 'EADDRINUSE') {\n`;
186
+ server += ` console.error('Port ' + port + ' is already in use. Stop the other process or set PORT to a different number.');\n`;
187
+ server += ` process.exit(1);\n`;
188
+ server += ` }\n`;
189
+ server += ` throw err;\n`;
190
+ server += `});\n`;
191
+
192
+ let types = `/**\n * Generated by Veko v1 AOT\n */\n\n`;
193
+ for (const block of ast.dataBlocks) {
194
+ types += `export interface ${block.name} {\n id: number;\n`;
195
+ for (const f of block.fields) {
196
+ const t = fieldToTsType(f);
197
+ types += ` ${f.name}${f.optional ? '?' : ''}: ${t};\n`;
198
+ }
199
+ types += `}\n\n`;
200
+ }
201
+
202
+ return { server, types };
203
+ }
@@ -0,0 +1,16 @@
1
+ export interface VekoConfig {
2
+ root: string;
3
+ entry: string;
4
+ output: { server: string; types: string };
5
+ adapter: string;
6
+ database: Record<string, unknown>;
7
+ server: { port?: number };
8
+ migrations: { table: string; dir: string };
9
+ plugins: unknown[];
10
+ }
11
+
12
+ export function loadConfig(rootDir?: string): Promise<VekoConfig>;
13
+
14
+ export function compile(
15
+ rootDir?: string
16
+ ): Promise<{ config: VekoConfig; output: { server: string; types: string } }>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Veko v1 – Compiler entry: load config, parse, generate, write.
3
+ * Used by cli/build.js and cli/dev.ts.
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { resolveModules } from './resolver.js';
9
+ import { generate } from './codegen.js';
10
+ import { loadConfig } from '../config/load-config.js';
11
+
12
+ // Re-export for CLI and programmatic use
13
+ export { loadConfig };
14
+
15
+ /**
16
+ * Compile api.vk → generated/server.js + types.d.ts.
17
+ * @param {string} [rootDir] - Project root
18
+ * @returns {{ config: object, output: { server: string, types: string } }}
19
+ * @throws {Error} on parse/generate/write failure
20
+ */
21
+ export async function compile(rootDir) {
22
+ const config = await loadConfig(rootDir);
23
+ const { entry, output, adapter } = config;
24
+ if (!fs.existsSync(entry)) {
25
+ throw new Error(`Entry file not found: ${entry}`);
26
+ }
27
+ const ast = resolveModules(entry, config.root);
28
+ const relRuntime = path.relative(path.dirname(output.server), config.root).replace(/\\/g, '/') || '.';
29
+ const { server, types } = generate(ast, {
30
+ adapter,
31
+ serverPath: output.server,
32
+ runtimeDir: path.join(relRuntime, 'src', 'runtime'),
33
+ });
34
+ const outDir = path.dirname(output.server);
35
+ if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
36
+ fs.writeFileSync(output.server, server, 'utf-8');
37
+ fs.writeFileSync(output.types, types, 'utf-8');
38
+ return { config, output: config.output };
39
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Veko v1 – Compiler module (public API for programmatic use)
3
+ */
4
+
5
+ export { compile, loadConfig } from './compile.js';