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/README.md +342 -0
- package/bin/luxaura.js +632 -0
- package/examples/TodoApp.lux +97 -0
- package/package.json +35 -0
- package/src/compiler/index.js +389 -0
- package/src/index.js +44 -0
- package/src/parser/index.js +319 -0
- package/src/runtime/luxaura.runtime.js +350 -0
- package/src/vault/server.js +207 -0
- package/templates/luxaura.config +43 -0
- package/ui-kit/luxaura.min.css +779 -0
- package/ui-kit/luxaura.min.js +271 -0
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);
|