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
|
@@ -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
|
+
};
|