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.
@@ -0,0 +1,97 @@
1
+ # TodoApp — Full Example .lux Application
2
+ # Showcases: state, server, style, view, events, bindings
3
+
4
+ props
5
+ title: String = "My Todos"
6
+
7
+ state
8
+ todos: []
9
+ newTodo: ""
10
+ filter: "all"
11
+ loading: false
12
+
13
+ server
14
+ import db from "luxaura/db"
15
+
16
+ def getTodos():
17
+ return db.query("SELECT * FROM todos ORDER BY created_at DESC")
18
+
19
+ def addTodo(text):
20
+ return db.insert("todos", { text, done: false, created_at: "NOW()" })
21
+
22
+ def toggleTodo(id, done):
23
+ return db.update("todos", { done }, { id })
24
+
25
+ def deleteTodo(id):
26
+ return db.query("DELETE FROM todos WHERE id = ?", [id])
27
+
28
+ style
29
+ self
30
+ padding: 8
31
+ background: #f8fafc
32
+ min-height: 100vh
33
+
34
+ Title
35
+ fontSize: 3xl
36
+ fontWeight: black
37
+ color: #1a1a2e
38
+ margin-bottom: 8
39
+
40
+ Input
41
+ padding: 4
42
+ radius: large
43
+ shadow: soft
44
+ fontSize: lg
45
+
46
+ Action
47
+ radius: large
48
+ padding: 4
49
+ shadow: soft
50
+ cursor: pointer
51
+
52
+ Card
53
+ shadow: medium
54
+ radius: large
55
+ padding: 6
56
+
57
+ view
58
+ Container
59
+ Column
60
+ Row
61
+ Title "{title}"
62
+ Badge "{todos.length} items"
63
+
64
+ Card
65
+ Row
66
+ Input type:"text" placeholder:"What needs to be done?" value:{newTodo}
67
+ Action "Add"
68
+ class: "lux-bg-primary lux-text-white"
69
+ on click:
70
+ await server.addTodo(newTodo)
71
+ newTodo = ""
72
+ todos = await server.getTodos()
73
+
74
+ Row
75
+ Action "All"
76
+ on click:
77
+ filter = "all"
78
+ Action "Active"
79
+ on click:
80
+ filter = "active"
81
+ Action "Done"
82
+ on click:
83
+ filter = "done"
84
+
85
+ List
86
+ ListItem
87
+ Row
88
+ Switch
89
+ Text "{todo.text}"
90
+ Action "Delete"
91
+ class: "lux-action danger"
92
+ on click:
93
+ await server.deleteTodo(todo.id)
94
+ todos = await server.getTodos()
95
+
96
+ Text "{todos.length === 0 ? 'No todos yet. Add one above!' : ''}"
97
+ class: "lux-text-muted lux-text-center"
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "luxaura",
3
+ "version": "1.0.0",
4
+ "description": "Intent-Based Full-Stack Web Framework — build apps with .lux files",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "luxaura": "./bin/luxaura.js"
8
+
9
+ },
10
+ "scripts": {
11
+ "test": "node test/run-tests.js"
12
+ },
13
+ "keywords": [
14
+ "framework",
15
+ "web",
16
+ "fullstack",
17
+ "lux",
18
+ "intent-based"
19
+ ],
20
+ "author": "Imed Rebhi",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "chokidar": "^3.5.3",
24
+ "commander": "^11.0.0",
25
+ "ws": "^8.14.2",
26
+ "express": "^4.18.2",
27
+ "http-proxy-middleware": "^2.0.6",
28
+ "chalk": "^4.1.2",
29
+ "glob": "^10.3.3",
30
+ "fs-extra": "^11.1.1"
31
+ },
32
+ "engines": {
33
+ "node": ">=18.0.0"
34
+ }
35
+ }
@@ -0,0 +1,389 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Luxaura Compiler
5
+ * Takes an AST produced by the parser and emits:
6
+ * - client.js : reactive runtime bundle (no server code)
7
+ * - server.js : secure RPC handler (vault)
8
+ * - index.html : entry HTML shell
9
+ */
10
+
11
+ const SMART_PROPS_MAP = {
12
+ padding: (v) => `padding: ${typeof v === 'number' ? v * 4 + 'px' : v};`,
13
+ margin: (v) => `margin: ${typeof v === 'number' ? v * 4 + 'px' : v};`,
14
+ radius: {
15
+ none: 'border-radius: 0;',
16
+ small: 'border-radius: 4px;',
17
+ medium: 'border-radius: 8px;',
18
+ large: 'border-radius: 16px;',
19
+ full: 'border-radius: 9999px;',
20
+ },
21
+ shadow: {
22
+ none: 'box-shadow: none;',
23
+ soft: 'box-shadow: 0 2px 12px rgba(0,0,0,0.08);',
24
+ medium: 'box-shadow: 0 4px 24px rgba(0,0,0,0.14);',
25
+ hard: 'box-shadow: 0 8px 32px rgba(0,0,0,0.24);',
26
+ },
27
+ gap: (v) => `gap: ${typeof v === 'number' ? v * 4 + 'px' : v};`,
28
+ width: (v) => `width: ${typeof v === 'number' ? v + 'px' : v};`,
29
+ height: (v) => `height: ${typeof v === 'number' ? v + 'px' : v};`,
30
+ color: (v) => `color: ${v};`,
31
+ background: (v) => `background: ${v};`,
32
+ fontSize: {
33
+ xs: 'font-size: 0.75rem;',
34
+ sm: 'font-size: 0.875rem;',
35
+ md: 'font-size: 1rem;',
36
+ lg: 'font-size: 1.25rem;',
37
+ xl: 'font-size: 1.5rem;',
38
+ '2xl': 'font-size: 2rem;',
39
+ '3xl': 'font-size: 3rem;',
40
+ },
41
+ fontWeight: {
42
+ thin: 'font-weight: 100;',
43
+ light: 'font-weight: 300;',
44
+ normal: 'font-weight: 400;',
45
+ medium: 'font-weight: 500;',
46
+ bold: 'font-weight: 700;',
47
+ black: 'font-weight: 900;',
48
+ },
49
+ display: (v) => `display: ${v};`,
50
+ flex: (v) => `flex: ${v};`,
51
+ align: (v) => `align-items: ${v};`,
52
+ justify: (v) => `justify-content: ${v};`,
53
+ overflow:(v) => `overflow: ${v};`,
54
+ opacity: (v) => `opacity: ${v};`,
55
+ border: (v) => `border: ${v};`,
56
+ cursor: (v) => `cursor: ${v};`,
57
+ zIndex: (v) => `z-index: ${v};`,
58
+ };
59
+
60
+ function resolveSmartProp(key, val) {
61
+ const handler = SMART_PROPS_MAP[key];
62
+ if (!handler) return `${camelToKebab(key)}: ${val};`;
63
+ if (typeof handler === 'function') return handler(val);
64
+ if (typeof handler === 'object') return handler[val] || `${camelToKebab(key)}: ${val};`;
65
+ return `${camelToKebab(key)}: ${val};`;
66
+ }
67
+
68
+ function camelToKebab(str) {
69
+ return str.replace(/([A-Z])/g, m => '-' + m.toLowerCase());
70
+ }
71
+
72
+ // ─── Component → HTML tag mapping ────────────────────────────────────────────
73
+
74
+ const COMPONENT_TAGS = {
75
+ Box: 'div',
76
+ Row: 'div',
77
+ Column: 'div',
78
+ Grid: 'div',
79
+ Container: 'div',
80
+ Nav: 'nav',
81
+ Sidebar: 'aside',
82
+ Footer: 'footer',
83
+ Header: 'header',
84
+ Section: 'section',
85
+ Title: 'h1', // runtime selects h1–h6 by context depth
86
+ Text: 'p',
87
+ Image: 'img',
88
+ Icon: 'span',
89
+ Action: 'button',
90
+ Input: 'input',
91
+ Form: 'form',
92
+ Switch: 'input', // type="checkbox"
93
+ Table: 'table',
94
+ List: 'ul',
95
+ ListItem: 'li',
96
+ Link: 'a',
97
+ Span: 'span',
98
+ Code: 'code',
99
+ Pre: 'pre',
100
+ Badge: 'span',
101
+ Card: 'div',
102
+ Modal: 'div',
103
+ Overlay: 'div',
104
+ Divider: 'hr',
105
+ };
106
+
107
+ const COMPONENT_DEFAULT_CLASSES = {
108
+ Box: 'lux-box',
109
+ Row: 'lux-row',
110
+ Column: 'lux-column',
111
+ Grid: 'lux-grid',
112
+ Container: 'lux-container',
113
+ Nav: 'lux-nav',
114
+ Sidebar: 'lux-sidebar',
115
+ Footer: 'lux-footer',
116
+ Header: 'lux-header',
117
+ Section: 'lux-section',
118
+ Title: 'lux-title',
119
+ Text: 'lux-text',
120
+ Image: 'lux-image',
121
+ Icon: 'lux-icon',
122
+ Action: 'lux-action',
123
+ Input: 'lux-input',
124
+ Form: 'lux-form',
125
+ Switch: 'lux-switch',
126
+ Table: 'lux-table',
127
+ List: 'lux-list',
128
+ ListItem: 'lux-list-item',
129
+ Link: 'lux-link',
130
+ Badge: 'lux-badge',
131
+ Card: 'lux-card',
132
+ };
133
+
134
+ // ─── Main Compiler Class ──────────────────────────────────────────────────────
135
+
136
+ class LuxCompiler {
137
+ constructor(ast, options = {}) {
138
+ this.ast = ast;
139
+ this.options = {
140
+ target: options.target || 'client', // 'client' | 'server' | 'full'
141
+ componentName: options.componentName || this._inferName(ast.filename),
142
+ ...options,
143
+ };
144
+ this._titleDepth = 0;
145
+ }
146
+
147
+ _inferName(filename) {
148
+ return (filename || 'App').replace(/\.lux$/, '').replace(/[^a-zA-Z0-9]/g, '');
149
+ }
150
+
151
+ compile() {
152
+ return {
153
+ clientJS: this._compileClientJS(),
154
+ serverJS: this._compileServerJS(),
155
+ css: this._compileCSS(),
156
+ html: this._compileHTMLShell(),
157
+ };
158
+ }
159
+
160
+ // ─── Client JS ─────────────────────────────────────────────────────────────
161
+
162
+ _compileClientJS() {
163
+ const name = this.options.componentName;
164
+ const stateObj = this._compileStateObj();
165
+ const propsArr = this._compilePropsDecl();
166
+ const render = this._compileRenderFn();
167
+ const events = this._compileEventSetup();
168
+
169
+ return `
170
+ /* Luxaura — ${name} Component | Generated by Luxaura Compiler v1.0 */
171
+ (function(Lux) {
172
+ Lux.define('${name}', {
173
+ props: [${propsArr}],
174
+ state: ${stateObj},
175
+ render(ctx) {
176
+ ${render}
177
+ },
178
+ mounted(el, ctx) {
179
+ ${events}
180
+ }
181
+ });
182
+ })(window.__Lux__);
183
+ `.trim();
184
+ }
185
+
186
+ _compileStateObj() {
187
+ if (!this.ast.state.length) return '{}';
188
+ const obj = {};
189
+ this.ast.state.forEach(s => { obj[s.name] = s.value; });
190
+ // Use JSON.stringify then indent for readability
191
+ return JSON.stringify(obj, null, 6).replace(/^/gm, ' ').trimStart();
192
+ }
193
+
194
+ _compilePropsDecl() {
195
+ return this.ast.props.map(p => `'${p.name}'`).join(', ');
196
+ }
197
+
198
+ _compileRenderFn() {
199
+ if (!this.ast.view) return " return '';";
200
+ const html = this._renderNode(this.ast.view, 0);
201
+ // Escape backticks in generated template
202
+ const escaped = html.replace(/`/g, '\\`');
203
+ return ` return \`${escaped}\`;`;
204
+ }
205
+
206
+ _renderNode(node, depth) {
207
+ if (!node) return '';
208
+ if (node.type === 'EventHandler') return '';
209
+
210
+ const tag = COMPONENT_TAGS[node.name] || node.name.toLowerCase();
211
+ const cls = COMPONENT_DEFAULT_CLASSES[node.name] || '';
212
+ const attrs = this._buildAttrs(node, cls);
213
+
214
+ // Special self-closing
215
+ if (tag === 'input' || tag === 'img' || tag === 'hr') {
216
+ return `<${tag}${attrs}>`;
217
+ }
218
+
219
+ // Title heading level based on nesting
220
+ let actualTag = tag;
221
+ if (node.name === 'Title') {
222
+ const level = Math.min(depth + 1, 6);
223
+ actualTag = `h${level}`;
224
+ }
225
+
226
+ const innerText = node.text
227
+ ? node.text
228
+ : node.binding
229
+ ? `\${ctx.get('${node.binding}')}`
230
+ : '';
231
+
232
+ const children = (node.children || [])
233
+ .filter(c => c.type !== 'EventHandler')
234
+ .map(c => this._renderNode(c, depth + 1))
235
+ .join('');
236
+
237
+ return `<${actualTag}${attrs}>${innerText}${children}</${actualTag}>`;
238
+ }
239
+
240
+ _buildAttrs(node, cls) {
241
+ const parts = [];
242
+ if (cls) parts.push(`class="${cls}"`);
243
+
244
+ // data-lux-id for event binding
245
+ parts.push(`data-lux-id="${node.name.toLowerCase()}-\${ctx._uid}"`);
246
+
247
+ // Image lazy loading
248
+ if (node.name === 'Image') {
249
+ parts.push('loading="lazy"');
250
+ if (node.props.src) parts.push(`src="${node.props.src}"`);
251
+ if (node.props.alt) parts.push(`alt="${node.props.alt}"`);
252
+ if (node.props.responsive) parts.push('style="max-width:100%;height:auto"');
253
+ }
254
+
255
+ // Input attributes
256
+ if (node.name === 'Input') {
257
+ const type = node.props.type || 'text';
258
+ parts.push(`type="${type}"`);
259
+ if (node.props.placeholder) parts.push(`placeholder="${node.props.placeholder}"`);
260
+ if (node.props.value) parts.push(`value="\${ctx.get('${node.props.value?.binding || node.props.value}')}" data-lux-bind="${node.props.value?.binding || node.props.value}"`);
261
+ }
262
+
263
+ // Switch
264
+ if (node.name === 'Switch') {
265
+ parts.push('type="checkbox"');
266
+ }
267
+
268
+ // Action / Link
269
+ if (node.name === 'Link' && node.props.href) {
270
+ parts.push(`href="${node.props.href}"`);
271
+ }
272
+
273
+ // Extra user-defined class
274
+ if (node.props.class) parts.push(`class="${cls} ${node.props.class}"`);
275
+
276
+ return parts.length ? ' ' + parts.join(' ') : '';
277
+ }
278
+
279
+ _compileEventSetup() {
280
+ if (!this.ast.view) return '';
281
+ const handlers = [];
282
+ this._collectEvents(this.ast.view, handlers);
283
+ if (!handlers.length) return ' // no events';
284
+ return handlers.map(h =>
285
+ ` Lux.on(el, '${h.component.toLowerCase()}', '${h.event}', async (ctx) => {\n ${h.body}\n });`
286
+ ).join('\n');
287
+ }
288
+
289
+ _collectEvents(node, out, parentName = null) {
290
+ if (!node) return;
291
+ const eventHandler = (node.children || []).find(c => c.type === 'EventHandler');
292
+ if (eventHandler) {
293
+ const body = (eventHandler.children || [])
294
+ .map(c => this._nodeToStatement(c))
295
+ .join('\n ');
296
+ out.push({ component: node.name, event: eventHandler.event, body: body || '/* no body */' });
297
+ }
298
+ (node.children || []).forEach(c => this._collectEvents(c, out, node.name));
299
+ }
300
+
301
+ _nodeToStatement(node) {
302
+ // Very simple: treat node name as a raw statement for event bodies
303
+ if (node.type === 'Component') {
304
+ // e.g. await server.saveData(count)
305
+ return node.name + (node.text ? ` ${node.text}` : '');
306
+ }
307
+ return '';
308
+ }
309
+
310
+ // ─── Server JS ─────────────────────────────────────────────────────────────
311
+
312
+ _compileServerJS() {
313
+ const name = this.options.componentName;
314
+ if (!this.ast.server) {
315
+ return `/* Luxaura Vault — ${name} | no server block */\nmodule.exports = {};`;
316
+ }
317
+
318
+ const { functions, raw } = this.ast.server;
319
+
320
+ // Translate `def name(params):` → `async function name(params) {`
321
+ let serverCode = raw
322
+ .replace(/^import\s+(\w+)\s+from\s+"([^"]+)"/gm, `const $1 = require('$2')`)
323
+ .replace(/^def\s+(\w+)\s*\(([^)]*)\)\s*:/gm, 'async function $1($2) {')
324
+ .replace(/^\s*db\.query\s*\(/gm, ' await db.query(');
325
+
326
+ // Close function blocks (simple heuristic: empty line after body)
327
+ serverCode = serverCode.replace(/\n\n/g, '\n}\n\n');
328
+
329
+ const exports = functions.map(f => ` ${f.name}`).join(',\n');
330
+
331
+ return `
332
+ /* Luxaura Vault — ${name} Server Module | NEVER sent to client */
333
+ 'use strict';
334
+ ${serverCode}
335
+
336
+ module.exports = {
337
+ ${exports || ' // no exported functions'}
338
+ };
339
+ `.trim();
340
+ }
341
+
342
+ // ─── CSS ───────────────────────────────────────────────────────────────────
343
+
344
+ _compileCSS() {
345
+ if (!this.ast.style.length) return '';
346
+ const name = this.options.componentName;
347
+ const blocks = this.ast.style.map(rule => {
348
+ const selector = this._resolveSelector(rule.selector, name);
349
+ const decls = Object.entries(rule.props)
350
+ .map(([k, v]) => ' ' + resolveSmartProp(k, v))
351
+ .join('\n');
352
+ return `${selector} {\n${decls}\n}`;
353
+ });
354
+ return `/* Luxaura — ${name} Styles */\n` + blocks.join('\n\n');
355
+ }
356
+
357
+ _resolveSelector(sel, componentName) {
358
+ if (sel === 'self') return `.lux-component-${componentName.toLowerCase()}`;
359
+ if (sel.startsWith('.') || sel.startsWith('#') || sel.startsWith('*')) return sel;
360
+ // Component name → default class
361
+ return COMPONENT_DEFAULT_CLASSES[sel] ? `.${COMPONENT_DEFAULT_CLASSES[sel].replace('lux-', 'lux-')}` : sel;
362
+ }
363
+
364
+ // ─── HTML Shell ────────────────────────────────────────────────────────────
365
+
366
+ _compileHTMLShell() {
367
+ const name = this.options.componentName;
368
+ return `<!DOCTYPE html>
369
+ <html lang="en">
370
+ <head>
371
+ <meta charset="UTF-8">
372
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
373
+ <title>${name}</title>
374
+ <link rel="stylesheet" href="/luxaura.min.css">
375
+ <link rel="stylesheet" href="/styles.css">
376
+ </head>
377
+ <body>
378
+ <div id="lux-root" data-component="${name}"></div>
379
+ <script src="/luxaura.min.js"></script>
380
+ <script src="/app.js"></script>
381
+ <script>
382
+ window.__Lux__.mount('${name}', '#lux-root');
383
+ </script>
384
+ </body>
385
+ </html>`;
386
+ }
387
+ }
388
+
389
+ module.exports = { LuxCompiler, resolveSmartProp, COMPONENT_TAGS };
package/src/index.js ADDED
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Luxaura Framework — Public API
5
+ */
6
+
7
+ const { LuxParser } = require('./parser');
8
+ const { LuxCompiler } = require('./compiler');
9
+ const { VaultServer } = require('./vault/server');
10
+
11
+ module.exports = {
12
+ LuxParser,
13
+ LuxCompiler,
14
+ VaultServer,
15
+
16
+ /**
17
+ * Parse a .lux source string and return an AST
18
+ * @param {string} source - .lux file contents
19
+ * @param {string} filename - optional filename for error messages
20
+ */
21
+ parse(source, filename = 'input.lux') {
22
+ return new LuxParser(source, filename).parse();
23
+ },
24
+
25
+ /**
26
+ * Compile an AST to client JS, server JS, and CSS
27
+ * @param {object} ast - AST from parse()
28
+ * @param {object} options - compilation options
29
+ */
30
+ compile(ast, options = {}) {
31
+ return new LuxCompiler(ast, options).compile();
32
+ },
33
+
34
+ /**
35
+ * Full pipeline: parse → compile
36
+ * @param {string} source - .lux source
37
+ * @param {string} filename
38
+ * @param {object} options
39
+ */
40
+ transform(source, filename = 'input.lux', options = {}) {
41
+ const ast = this.parse(source, filename);
42
+ return this.compile(ast, options);
43
+ },
44
+ };