luxaura 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/bin/luxaura.js ADDED
@@ -0,0 +1,632 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Luxaura CLI
6
+ * Commands: new, run, build, install, generate, format, secure
7
+ */
8
+
9
+ const { program } = require('commander');
10
+ const chalk = require('chalk');
11
+ const fs = require('fs-extra');
12
+ const path = require('path');
13
+ const { execSync } = require('child_process');
14
+ const chokidar = require('chokidar');
15
+
16
+ const { LuxParser } = require('../src/parser');
17
+ const { LuxCompiler } = require('../src/compiler');
18
+ const { VaultServer } = require('../src/vault/server');
19
+
20
+ const VERSION = '1.0.0';
21
+
22
+ // ─── Banner ──────────────────────────────────────────────────────────────────
23
+
24
+ function banner() {
25
+ console.log(chalk.cyan(`
26
+ ██╗ ██╗ ██╗██╗ ██╗ █████╗ ██╗ ██╗██████╗ █████╗
27
+ ██║ ██║ ██║╚██╗██╔╝██╔══██╗██║ ██║██╔══██╗██╔══██╗
28
+ ██║ ██║ ██║ ╚███╔╝ ███████║██║ ██║██████╔╝███████║
29
+ ██║ ██║ ██║ ██╔██╗ ██╔══██║██║ ██║██╔══██╗██╔══██║
30
+ ███████╗╚██████╔╝██╔╝ ██╗██║ ██║╚██████╔╝██║ ██║██║ ██║
31
+ ╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝
32
+ `));
33
+ console.log(chalk.gray(` Intent-Based Web Framework v${VERSION}\n`));
34
+ }
35
+
36
+ // ─── Helpers ─────────────────────────────────────────────────────────────────
37
+
38
+ function log(msg) { console.log(chalk.cyan(' ▸'), msg); }
39
+ function success(msg) { console.log(chalk.green(' ✔'), msg); }
40
+ function warn(msg) { console.log(chalk.yellow(' ⚠'), msg); }
41
+ function error(msg) { console.error(chalk.red(' ✖'), msg); }
42
+ function info(msg) { console.log(chalk.gray(' ·'), msg); }
43
+
44
+ function compileLuxFile(filePath, options = {}) {
45
+ const source = fs.readFileSync(filePath, 'utf8');
46
+ const filename = path.basename(filePath);
47
+ const parser = new LuxParser(source, filename);
48
+ const ast = parser.parse();
49
+ const compiler = new LuxCompiler(ast, options);
50
+ return compiler.compile();
51
+ }
52
+
53
+ function getAllLuxFiles(dir) {
54
+ const results = [];
55
+ function walk(d) {
56
+ if (!fs.existsSync(d)) return;
57
+ for (const entry of fs.readdirSync(d, { withFileTypes: true })) {
58
+ const full = path.join(d, entry.name);
59
+ if (entry.isDirectory() && !['node_modules', 'dist', '.git'].includes(entry.name)) {
60
+ walk(full);
61
+ } else if (entry.isFile() && entry.name.endsWith('.lux')) {
62
+ results.push(full);
63
+ }
64
+ }
65
+ }
66
+ walk(dir);
67
+ return results;
68
+ }
69
+
70
+ // ─── Command: new ─────────────────────────────────────────────────────────────
71
+
72
+ program
73
+ .command('new <name>')
74
+ .description('Create a new Luxaura project')
75
+ .action(async (name) => {
76
+ banner();
77
+ const projectDir = path.resolve(process.cwd(), name);
78
+
79
+ if (fs.existsSync(projectDir)) {
80
+ error(`Directory "${name}" already exists.`);
81
+ process.exit(1);
82
+ }
83
+
84
+ log(`Creating project: ${chalk.white(name)}`);
85
+
86
+ // Directory structure
87
+ const dirs = [
88
+ projectDir,
89
+ path.join(projectDir, 'pages'),
90
+ path.join(projectDir, 'modules'),
91
+ path.join(projectDir, 'assets'),
92
+ path.join(projectDir, 'server'),
93
+ path.join(projectDir, 'dist', 'client'),
94
+ path.join(projectDir, 'dist', 'server'),
95
+ ];
96
+ dirs.forEach(d => fs.mkdirpSync(d));
97
+
98
+ // luxaura.config
99
+ fs.writeFileSync(path.join(projectDir, 'luxaura.config'), `
100
+ # Luxaura Configuration
101
+ app.name: ${name}
102
+ app.version: 1.0.0
103
+
104
+ # theme: light | dark
105
+ theme: light
106
+
107
+ # mode: full | headless
108
+ mode: full
109
+
110
+ # Database (optional)
111
+ # db.type: postgres
112
+ # db.url: postgresql://localhost:5432/${name}
113
+
114
+ # Headless proxy (only used when mode: headless)
115
+ # proxy: http://localhost:8080
116
+ `.trim() + '\n');
117
+
118
+ // pages/index.lux
119
+ fs.writeFileSync(path.join(projectDir, 'pages', 'index.lux'), `
120
+ # ${name} — Home Page
121
+
122
+ props
123
+ title: String = "Welcome to ${name}"
124
+
125
+ state
126
+ count: 0
127
+
128
+ style
129
+ self
130
+ padding: 8
131
+ background: #ffffff
132
+
133
+ Title
134
+ fontSize: 2xl
135
+ fontWeight: bold
136
+ color: #1a1a2e
137
+
138
+ Action
139
+ padding: 4
140
+ radius: medium
141
+ background: #6c63ff
142
+ color: #ffffff
143
+ cursor: pointer
144
+
145
+ view
146
+ Container
147
+ Title "{title}"
148
+ Text "You have clicked the button {count} times."
149
+ Action "Click Me"
150
+ on click:
151
+ await server.logClick(count)
152
+ `.trim() + '\n');
153
+
154
+ // modules/Navbar.lux
155
+ fs.writeFileSync(path.join(projectDir, 'modules', 'Navbar.lux'), `
156
+ # Reusable Navbar Component
157
+
158
+ props
159
+ brand: String = "${name}"
160
+
161
+ style
162
+ self
163
+ padding: 4
164
+ background: #1a1a2e
165
+ shadow: soft
166
+
167
+ Title
168
+ color: #ffffff
169
+ fontSize: lg
170
+ fontWeight: bold
171
+
172
+ view
173
+ Nav
174
+ Row
175
+ Title "{brand}"
176
+ Row
177
+ Action "Home"
178
+ Action "About"
179
+ Action "Contact"
180
+ `.trim() + '\n');
181
+
182
+ // server/index.js
183
+ fs.writeFileSync(path.join(projectDir, 'server', 'index.js'), `
184
+ 'use strict';
185
+ // Shared server utilities (imported by compiled server modules)
186
+
187
+ async function logClick(count) {
188
+ console.log('[Server] Button clicked, count:', count);
189
+ return { ok: true, count };
190
+ }
191
+
192
+ module.exports = { logClick };
193
+ `);
194
+
195
+ // package.json for project
196
+ fs.writeFileSync(path.join(projectDir, 'package.json'), JSON.stringify({
197
+ name,
198
+ version: '1.0.0',
199
+ description: `${name} — built with Luxaura`,
200
+ scripts: {
201
+ dev: 'luxaura run',
202
+ build: 'luxaura build',
203
+ },
204
+ dependencies: {
205
+ luxaura: `^${VERSION}`,
206
+ },
207
+ }, null, 2));
208
+
209
+ // .gitignore
210
+ fs.writeFileSync(path.join(projectDir, '.gitignore'), `node_modules/\ndist/\n.DS_Store\n`);
211
+
212
+ // Copy UI kit files if they exist (they'll be generated later)
213
+ const uiKitSrc = path.join(__dirname, '..', 'ui-kit');
214
+ const uiKitDst = path.join(projectDir, 'dist', 'client');
215
+ if (fs.existsSync(path.join(uiKitSrc, 'luxaura.min.css'))) {
216
+ fs.copySync(path.join(uiKitSrc, 'luxaura.min.css'), path.join(uiKitDst, 'luxaura.min.css'));
217
+ fs.copySync(path.join(uiKitSrc, 'luxaura.min.js'), path.join(uiKitDst, 'luxaura.min.js'));
218
+ }
219
+
220
+ success(`Project "${name}" created!`);
221
+ info(`\n cd ${name}`);
222
+ info(` luxaura run\n`);
223
+ });
224
+
225
+ // ─── Command: run ─────────────────────────────────────────────────────────────
226
+
227
+ program
228
+ .command('run')
229
+ .description('Start development server with HMR')
230
+ .option('-p, --port <port>', 'Port number', '3000')
231
+ .action(async (opts) => {
232
+ banner();
233
+ const rootDir = process.cwd();
234
+ const port = parseInt(opts.port, 10);
235
+
236
+ log('Compiling .lux files...');
237
+ await buildAll(rootDir, { dev: true });
238
+
239
+ const vault = new VaultServer({ port, rootDir });
240
+ const actualPort = await vault.start();
241
+ success(`Dev server running at ${chalk.white(`http://localhost:${actualPort}`)}`);
242
+ info('Hot Module Replacement enabled. Watching for changes...\n');
243
+
244
+ // Inject HMR client script into index.html
245
+ _injectHMR(rootDir, actualPort);
246
+
247
+ const watcher = chokidar.watch(
248
+ [path.join(rootDir, 'pages'), path.join(rootDir, 'modules')],
249
+ { ignoreInitial: true }
250
+ );
251
+
252
+ watcher.on('change', async (filePath) => {
253
+ log(`Changed: ${chalk.yellow(path.relative(rootDir, filePath))}`);
254
+ try {
255
+ await buildFile(filePath, rootDir, { dev: true });
256
+ vault.triggerHMR(path.relative(rootDir, filePath));
257
+ success('Rebuilt and reloaded.');
258
+ } catch (e) {
259
+ error(`Compile error: ${e.message}`);
260
+ }
261
+ });
262
+
263
+ watcher.on('add', async (filePath) => {
264
+ log(`New file: ${chalk.yellow(path.relative(rootDir, filePath))}`);
265
+ await buildFile(filePath, rootDir, { dev: true });
266
+ vault.triggerHMR('new-file');
267
+ });
268
+ });
269
+
270
+ function _injectHMR(rootDir, port) {
271
+ const indexPath = path.join(rootDir, 'dist', 'client', 'index.html');
272
+ if (!fs.existsSync(indexPath)) return;
273
+ let html = fs.readFileSync(indexPath, 'utf8');
274
+ if (html.includes('__lux_hmr__')) return;
275
+ const hmrScript = `
276
+ <script id="__lux_hmr__">
277
+ (function(){
278
+ const ws = new WebSocket('ws://localhost:${port}');
279
+ ws.onmessage = function(e) {
280
+ const msg = JSON.parse(e.data);
281
+ if (msg.type === 'hmr') {
282
+ console.log('[Luxaura HMR] Reloading:', msg.file);
283
+ location.reload();
284
+ }
285
+ };
286
+ ws.onclose = function() {
287
+ console.log('[Luxaura HMR] Connection lost. Reconnecting...');
288
+ setTimeout(() => location.reload(), 2000);
289
+ };
290
+ })();
291
+ </script>`;
292
+ html = html.replace('</body>', hmrScript + '</body>');
293
+ fs.writeFileSync(indexPath, html);
294
+ }
295
+
296
+ // ─── Command: build ───────────────────────────────────────────────────────────
297
+
298
+ program
299
+ .command('build')
300
+ .description('Compile for production (splits client/server)')
301
+ .action(async () => {
302
+ banner();
303
+ const rootDir = process.cwd();
304
+ log('Building for production...');
305
+
306
+ const start = Date.now();
307
+ const stats = await buildAll(rootDir, { dev: false });
308
+
309
+ success(`Build complete in ${Date.now() - start}ms`);
310
+ info(` Client assets → ${chalk.white('dist/client/')}`);
311
+ info(` Server modules → ${chalk.white('dist/server/')}`);
312
+ info(` Files compiled: ${stats.count}`);
313
+ if (stats.errors.length) {
314
+ stats.errors.forEach(e => error(e));
315
+ }
316
+ });
317
+
318
+ // ─── Command: install ─────────────────────────────────────────────────────────
319
+
320
+ program
321
+ .command('install <lib>')
322
+ .description('Install and integrate a frontend library')
323
+ .action(async (lib) => {
324
+ banner();
325
+ log(`Installing ${chalk.yellow(lib)}...`);
326
+ try {
327
+ execSync(`npm install ${lib}`, { stdio: 'inherit', cwd: process.cwd() });
328
+ success(`${lib} installed.`);
329
+ info('Classes are now available in your .lux files via the class: attribute.');
330
+ } catch (e) {
331
+ error(`npm install failed: ${e.message}`);
332
+ }
333
+ });
334
+
335
+ // ─── Command: generate ────────────────────────────────────────────────────────
336
+
337
+ const TEMPLATES = {
338
+ component: (name) => `
339
+ # ${name} Component
340
+
341
+ props
342
+ label: String = "${name}"
343
+
344
+ state
345
+ active: false
346
+
347
+ style
348
+ self
349
+ padding: 4
350
+ radius: medium
351
+ shadow: soft
352
+
353
+ view
354
+ Box
355
+ Title "{label}"
356
+ Text "Edit ${name}.lux to get started."
357
+ `.trim() + '\n',
358
+
359
+ page: (name) => `
360
+ # ${name} Page
361
+
362
+ props
363
+ title: String = "${name}"
364
+
365
+ state
366
+ loading: false
367
+
368
+ style
369
+ self
370
+ padding: 8
371
+
372
+ view
373
+ Container
374
+ Title "{title}"
375
+ Text "This is the ${name} page."
376
+ `.trim() + '\n',
377
+
378
+ api: (name) => `
379
+ # ${name} API Module (server-side only)
380
+ # This file is compiled into dist/server and NEVER sent to the client.
381
+
382
+ server
383
+ import db from "luxaura/db"
384
+
385
+ def get${name}(id):
386
+ return db.query("SELECT * FROM ${name.toLowerCase()}s WHERE id = ?", [id])
387
+
388
+ def create${name}(data):
389
+ return db.insert("${name.toLowerCase()}s", data)
390
+
391
+ def update${name}(id, data):
392
+ return db.update("${name.toLowerCase()}s", data, { id })
393
+ `.trim() + '\n',
394
+ };
395
+
396
+ program
397
+ .command('generate <type> <name>')
398
+ .alias('g')
399
+ .description('Scaffold component, page, or api file')
400
+ .action((type, name) => {
401
+ banner();
402
+ const rootDir = process.cwd();
403
+ const template = TEMPLATES[type];
404
+ if (!template) {
405
+ error(`Unknown type "${type}". Use: component, page, api`);
406
+ process.exit(1);
407
+ }
408
+
409
+ const dirs = { component: 'modules', page: 'pages', api: 'pages' };
410
+ const dir = path.join(rootDir, dirs[type]);
411
+ fs.mkdirpSync(dir);
412
+
413
+ const filePath = path.join(dir, `${name}.lux`);
414
+ if (fs.existsSync(filePath)) {
415
+ warn(`File already exists: ${path.relative(rootDir, filePath)}`);
416
+ process.exit(0);
417
+ }
418
+
419
+ fs.writeFileSync(filePath, template(name));
420
+ success(`Generated: ${chalk.white(path.relative(rootDir, filePath))}`);
421
+ });
422
+
423
+ // ─── Command: format ──────────────────────────────────────────────────────────
424
+
425
+ program
426
+ .command('format')
427
+ .description('Auto-fix indentation and style in .lux files')
428
+ .action(() => {
429
+ banner();
430
+ const rootDir = process.cwd();
431
+ const files = getAllLuxFiles(rootDir);
432
+ let fixed = 0;
433
+
434
+ files.forEach(filePath => {
435
+ try {
436
+ const source = fs.readFileSync(filePath, 'utf8');
437
+ const formatted = formatLux(source);
438
+ if (formatted !== source) {
439
+ fs.writeFileSync(filePath, formatted);
440
+ log(`Formatted: ${path.relative(rootDir, filePath)}`);
441
+ fixed++;
442
+ }
443
+ } catch (e) {
444
+ warn(`Could not format ${filePath}: ${e.message}`);
445
+ }
446
+ });
447
+
448
+ success(`Formatted ${fixed} file(s).`);
449
+ });
450
+
451
+ function formatLux(source) {
452
+ const BLOCK_NAMES = ['props', 'state', 'server', 'style', 'view'];
453
+ const lines = source.split('\n');
454
+ const out = [];
455
+ let inBlock = false;
456
+
457
+ for (let i = 0; i < lines.length; i++) {
458
+ const raw = lines[i];
459
+ const trimmed = raw.trim();
460
+
461
+ if (!trimmed) { out.push(''); continue; }
462
+ if (trimmed.startsWith('#')) { out.push(trimmed); continue; }
463
+
464
+ if (BLOCK_NAMES.includes(trimmed)) {
465
+ if (i > 0) out.push('');
466
+ out.push(trimmed);
467
+ inBlock = true;
468
+ continue;
469
+ }
470
+
471
+ if (inBlock) {
472
+ // Normalize to 4-space indent per level
473
+ const indent = raw.match(/^(\s*)/)[1];
474
+ const spaces = indent.replace(/\t/g, ' ');
475
+ const level = Math.round(spaces.length / 4);
476
+ out.push(' '.repeat(Math.max(1, level)) + trimmed);
477
+ } else {
478
+ out.push(raw);
479
+ }
480
+ }
481
+
482
+ return out.join('\n').trimEnd() + '\n';
483
+ }
484
+
485
+ // ─── Command: secure ──────────────────────────────────────────────────────────
486
+
487
+ program
488
+ .command('secure')
489
+ .description('Scan codebase for security vulnerabilities')
490
+ .action(() => {
491
+ banner();
492
+ const rootDir = process.cwd();
493
+ const files = getAllLuxFiles(rootDir);
494
+ let issues = 0;
495
+
496
+ const XSS_PATTERNS = [
497
+ { re: /innerHTML\s*=/g, msg: 'Unsafe innerHTML assignment (XSS risk)' },
498
+ { re: /document\.write\s*\(/g, msg: 'document.write() usage (XSS risk)' },
499
+ { re: /eval\s*\(/g, msg: 'eval() usage (code injection risk)' },
500
+ ];
501
+
502
+ const SQL_PATTERNS = [
503
+ { re: /db\.query\s*\(\s*`[^`]*\$\{/g, msg: 'Possible SQL injection: template literal in query. Use parameterized queries.' },
504
+ { re: /db\.query\s*\(\s*"[^"]*"\s*\+/g, msg: 'Possible SQL injection: string concatenation in query. Use parameterized queries.' },
505
+ ];
506
+
507
+ const SERVER_LEAK = [
508
+ { re: /process\.env\.(DB_PASSWORD|SECRET|API_KEY)/gi, msg: 'Sensitive env var referenced — verify it is in server block only' },
509
+ ];
510
+
511
+ files.forEach(filePath => {
512
+ const source = fs.readFileSync(filePath, 'utf8');
513
+ const relPath = path.relative(rootDir, filePath);
514
+
515
+ const allPatterns = [...XSS_PATTERNS, ...SQL_PATTERNS, ...SERVER_LEAK];
516
+ allPatterns.forEach(({ re, msg }) => {
517
+ const matches = source.match(re);
518
+ if (matches) {
519
+ warn(`${relPath}: ${msg}`);
520
+ issues++;
521
+ }
522
+ });
523
+
524
+ // Check for server code outside server block
525
+ const serverBlockMatch = source.match(/^server$([\s\S]*?)(?=^(?:props|state|style|view)$|\Z)/m);
526
+ if (!serverBlockMatch) {
527
+ const dbOutsideServer = /db\.(query|insert|update)/.test(source);
528
+ if (dbOutsideServer) {
529
+ error(`${relPath}: db access detected outside server block — this may leak to client`);
530
+ issues++;
531
+ }
532
+ }
533
+ });
534
+
535
+ if (issues === 0) {
536
+ success(`No issues found. ${files.length} files scanned.`);
537
+ } else {
538
+ warn(`Found ${issues} potential issue(s) across ${files.length} files.`);
539
+ }
540
+ });
541
+
542
+ // ─── Build Helpers ─────────────────────────────────────────────────────────────
543
+
544
+ async function buildAll(rootDir, opts = {}) {
545
+ const files = getAllLuxFiles(rootDir);
546
+ const errors = [];
547
+ let count = 0;
548
+
549
+ // Copy UI kit
550
+ const uiKitSrc = path.join(__dirname, '..', 'ui-kit');
551
+ const clientDir = path.join(rootDir, 'dist', 'client');
552
+ fs.mkdirpSync(clientDir);
553
+ fs.mkdirpSync(path.join(rootDir, 'dist', 'server'));
554
+
555
+ if (fs.existsSync(path.join(uiKitSrc, 'luxaura.min.css'))) {
556
+ fs.copySync(path.join(uiKitSrc, 'luxaura.min.css'), path.join(clientDir, 'luxaura.min.css'));
557
+ fs.copySync(path.join(uiKitSrc, 'luxaura.min.js'), path.join(clientDir, 'luxaura.min.js'));
558
+ }
559
+
560
+ // Copy static assets
561
+ const assetsDir = path.join(rootDir, 'assets');
562
+ if (fs.existsSync(assetsDir)) {
563
+ fs.copySync(assetsDir, path.join(clientDir, 'assets'));
564
+ }
565
+
566
+ for (const f of files) {
567
+ try {
568
+ await buildFile(f, rootDir, opts);
569
+ count++;
570
+ } catch (e) {
571
+ errors.push(`${path.relative(rootDir, f)}: ${e.message}`);
572
+ }
573
+ }
574
+
575
+ return { count, errors };
576
+ }
577
+
578
+ async function buildFile(filePath, rootDir, opts = {}) {
579
+ const source = fs.readFileSync(filePath, 'utf8');
580
+ const filename = path.basename(filePath);
581
+ const parser = new LuxParser(source, filename);
582
+ const ast = parser.parse();
583
+ const compiler = new LuxCompiler(ast);
584
+ const output = compiler.compile();
585
+
586
+ const clientDir = path.join(rootDir, 'dist', 'client');
587
+ const serverDir = path.join(rootDir, 'dist', 'server');
588
+ const name = filename.replace('.lux', '');
589
+
590
+ fs.mkdirpSync(clientDir);
591
+ fs.mkdirpSync(serverDir);
592
+
593
+ // Client JS
594
+ fs.writeFileSync(path.join(clientDir, `${name}.component.js`), output.clientJS);
595
+
596
+ // Server JS (vault)
597
+ if (output.serverJS && output.serverJS !== `/* Luxaura Vault — ${name} | no server block */\nmodule.exports = {};`) {
598
+ fs.writeFileSync(path.join(serverDir, `${name}.js`), output.serverJS);
599
+ }
600
+
601
+ // Component CSS
602
+ if (output.css) {
603
+ const cssPath = path.join(clientDir, 'styles.css');
604
+ const existing = fs.existsSync(cssPath) ? fs.readFileSync(cssPath, 'utf8') : '';
605
+ if (!existing.includes(`/* Luxaura — ${name} Styles */`)) {
606
+ fs.appendFileSync(cssPath, '\n' + output.css);
607
+ }
608
+ }
609
+
610
+ // HTML shell (only for pages)
611
+ if (filePath.includes('/pages/')) {
612
+ const htmlPath = path.join(clientDir, name === 'index' ? 'index.html' : `${name}.html`);
613
+ fs.writeFileSync(htmlPath, output.html);
614
+
615
+ // Bundle app.js
616
+ const appJsPath = path.join(clientDir, 'app.js');
617
+ const componentFiles = fs.readdirSync(clientDir)
618
+ .filter(f => f.endsWith('.component.js'))
619
+ .map(f => `// Component: ${f}\n` + fs.readFileSync(path.join(clientDir, f), 'utf8'))
620
+ .join('\n\n');
621
+ fs.writeFileSync(appJsPath, componentFiles);
622
+ }
623
+ }
624
+
625
+ // ─── Run ─────────────────────────────────────────────────────────────────────
626
+
627
+ program
628
+ .name('luxaura')
629
+ .version(VERSION)
630
+ .description('Luxaura Framework CLI');
631
+
632
+ program.parse(process.argv);