juxscript 1.0.89 → 1.0.91

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,638 @@
1
+ import * as esbuild from 'esbuild';
2
+ import * as acorn from 'acorn';
3
+ import { walk } from 'astray';
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
10
+
11
+ export class JuxCompiler {
12
+ constructor(config = {}) {
13
+ this.config = config;
14
+ this.srcDir = config.srcDir || './jux';
15
+ this.distDir = config.distDir || './.jux-dist';
16
+ this._juxscriptExports = null;
17
+ this._juxscriptPath = null;
18
+ }
19
+
20
+ /**
21
+ * Locate juxscript package - simplified resolution
22
+ */
23
+ findJuxscriptPath() {
24
+ if (this._juxscriptPath) return this._juxscriptPath;
25
+
26
+ const projectRoot = process.cwd();
27
+
28
+ // Priority 1: User's node_modules (when used as dependency)
29
+ const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
30
+ if (fs.existsSync(userPath)) {
31
+ this._juxscriptPath = userPath;
32
+ return userPath;
33
+ }
34
+
35
+ // Priority 2: Package root (when developing juxscript itself)
36
+ const packageRoot = path.resolve(__dirname, '..');
37
+ const devPath = path.resolve(packageRoot, 'index.js');
38
+ if (fs.existsSync(devPath)) {
39
+ this._juxscriptPath = devPath;
40
+ return devPath;
41
+ }
42
+
43
+ // Priority 3: Sibling in monorepo
44
+ const monoPath = path.resolve(projectRoot, '../jux/index.js');
45
+ if (fs.existsSync(monoPath)) {
46
+ this._juxscriptPath = monoPath;
47
+ return monoPath;
48
+ }
49
+
50
+ return null;
51
+ }
52
+
53
+ /**
54
+ * Get the components directory for CSS copying
55
+ */
56
+ getComponentsDir() {
57
+ const juxPath = this.findJuxscriptPath();
58
+ if (!juxPath) return null;
59
+ return path.resolve(path.dirname(juxPath), 'lib/componentsv2');
60
+ }
61
+
62
+ scanFiles() {
63
+ const files = fs.readdirSync(this.srcDir)
64
+ .filter(f => f.endsWith('.jux') || f.endsWith('.js'));
65
+
66
+ const views = [], dataModules = [], sharedModules = [];
67
+
68
+ files.forEach(file => {
69
+ const content = fs.readFileSync(path.join(this.srcDir, file), 'utf8');
70
+ const name = file.replace(/\.[^/.]+$/, '');
71
+
72
+ if (file.includes('data')) {
73
+ dataModules.push({ name, file, content });
74
+ } else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
75
+ sharedModules.push({ name, file, content });
76
+ } else {
77
+ views.push({ name, file, content });
78
+ }
79
+ });
80
+
81
+ return { views, dataModules, sharedModules };
82
+ }
83
+
84
+ removeImports(code) {
85
+ return code
86
+ .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
87
+ .replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
88
+ .replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
89
+ .replace(/^\s*import\s*;?\s*$/gm, '');
90
+ }
91
+
92
+ sanitizeName(name) {
93
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
94
+ }
95
+
96
+ copyJuxscriptCss() {
97
+ const componentsDir = this.getComponentsDir();
98
+ if (!componentsDir) return [];
99
+
100
+ const cssFiles = [];
101
+
102
+ const walkDir = (dir, relativePath = '') => {
103
+ if (!fs.existsSync(dir)) return;
104
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
105
+ for (const entry of entries) {
106
+ const fullPath = path.join(dir, entry.name);
107
+ const relPath = path.join(relativePath, entry.name);
108
+ if (entry.isDirectory()) {
109
+ walkDir(fullPath, relPath);
110
+ } else if (entry.name === 'structure.css') {
111
+ cssFiles.push({ src: fullPath, dest: relPath, component: relativePath });
112
+ }
113
+ }
114
+ };
115
+
116
+ walkDir(componentsDir);
117
+
118
+ const cssDistDir = path.join(this.distDir, 'css');
119
+ fs.mkdirSync(cssDistDir, { recursive: true });
120
+
121
+ cssFiles.forEach(css => {
122
+ const destPath = path.join(cssDistDir, css.dest);
123
+ fs.mkdirSync(path.dirname(destPath), { recursive: true });
124
+ fs.copyFileSync(css.src, destPath);
125
+ });
126
+
127
+ console.log(`šŸ“„ Copied ${cssFiles.length} CSS files`);
128
+ return cssFiles;
129
+ }
130
+
131
+ generateCssLinks(cssFiles) {
132
+ return cssFiles.map(css => ` <link rel="stylesheet" href="./css/${css.dest}">`).join('\n');
133
+ }
134
+
135
+ async loadJuxscriptExports() {
136
+ if (this._juxscriptExports) return this._juxscriptExports;
137
+
138
+ try {
139
+ const juxscriptPath = this.findJuxscriptPath();
140
+ if (juxscriptPath) {
141
+ const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
142
+ const exports = new Set();
143
+
144
+ // Parse export statements
145
+ for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
146
+ match[1].split(',').forEach(exp => {
147
+ const name = exp.trim().split(/\s+as\s+/)[0].trim();
148
+ if (name) exports.add(name);
149
+ });
150
+ }
151
+
152
+ this._juxscriptExports = [...exports];
153
+ if (this._juxscriptExports.length > 0) {
154
+ console.log(`šŸ“¦ juxscript exports: ${this._juxscriptExports.join(', ')}`);
155
+ }
156
+ }
157
+ } catch (err) {
158
+ this._juxscriptExports = [];
159
+ }
160
+
161
+ return this._juxscriptExports;
162
+ }
163
+
164
+ validateViewCode(viewName, code) {
165
+ const issues = [];
166
+
167
+ let ast;
168
+ try {
169
+ ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
170
+ } catch (parseError) {
171
+ issues.push({
172
+ type: 'error',
173
+ view: viewName,
174
+ line: parseError.loc?.line || 0,
175
+ message: `Syntax error: ${parseError.message}`,
176
+ code: ''
177
+ });
178
+ return issues;
179
+ }
180
+
181
+ const juxscriptImports = new Set();
182
+ const allImports = new Set();
183
+
184
+ walk(ast, {
185
+ ImportDeclaration(node) {
186
+ node.specifiers.forEach(spec => {
187
+ allImports.add(spec.local.name);
188
+ if (node.source.value === 'juxscript') {
189
+ juxscriptImports.add(spec.local.name);
190
+ }
191
+ });
192
+ }
193
+ });
194
+
195
+ const knownComponents = this._juxscriptExports || ['Element', 'Grid', 'Input', 'List'];
196
+
197
+ walk(ast, {
198
+ Identifier(node, parent) {
199
+ if (parent?.type === 'CallExpression' && parent.callee === node) {
200
+ const name = node.name;
201
+ if (/^[A-Z]/.test(name) && !allImports.has(name) && knownComponents.includes(name)) {
202
+ issues.push({
203
+ type: 'warning',
204
+ view: viewName,
205
+ line: node.loc?.start?.line || 0,
206
+ message: `"${name}" is used but not imported from juxscript`,
207
+ code: ''
208
+ });
209
+ }
210
+ }
211
+ }
212
+ });
213
+
214
+ return issues;
215
+ }
216
+
217
+ generateEntryPoint(views, dataModules, sharedModules) {
218
+ let entry = `// Auto-generated JUX entry point\n\n`;
219
+ const allIssues = [];
220
+ const sourceSnapshot = {};
221
+
222
+ const juxImports = new Set();
223
+ [...views, ...dataModules, ...sharedModules].forEach(m => {
224
+ for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
225
+ match[1].split(',').map(s => s.trim()).forEach(imp => {
226
+ if (imp) juxImports.add(imp);
227
+ });
228
+ }
229
+ });
230
+
231
+ if (juxImports.size > 0) {
232
+ entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
233
+ }
234
+
235
+ dataModules.forEach(m => {
236
+ entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
237
+ });
238
+ sharedModules.forEach(m => {
239
+ entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
240
+ });
241
+
242
+ entry += `\n// Expose to window\n`;
243
+ dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
244
+ sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
245
+
246
+ if (juxImports.size > 0) {
247
+ entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
248
+ }
249
+
250
+ entry += `\n// --- VIEW FUNCTIONS ---\n`;
251
+
252
+ views.forEach(v => {
253
+ const capitalized = v.name.charAt(0).toUpperCase() + v.name.slice(1);
254
+ allIssues.push(...this.validateViewCode(v.name, v.content));
255
+
256
+ sourceSnapshot[v.file] = {
257
+ name: v.name,
258
+ file: v.file,
259
+ content: v.content,
260
+ lines: v.content.split('\n')
261
+ };
262
+
263
+ let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
264
+ const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
265
+
266
+ entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
267
+ });
268
+
269
+ dataModules.forEach(m => {
270
+ sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
271
+ });
272
+ sharedModules.forEach(m => {
273
+ sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
274
+ });
275
+
276
+ this._sourceSnapshot = sourceSnapshot;
277
+ this._validationIssues = allIssues;
278
+ entry += this._generateRouter(views);
279
+ return entry;
280
+ }
281
+
282
+ reportValidationIssues() {
283
+ const issues = this._validationIssues || [];
284
+ const errors = issues.filter(i => i.type === 'error');
285
+ const warnings = issues.filter(i => i.type === 'warning');
286
+
287
+ if (issues.length > 0) {
288
+ console.log('\nāš ļø Validation Issues:\n');
289
+ issues.forEach(issue => {
290
+ const icon = issue.type === 'error' ? 'āŒ' : 'āš ļø';
291
+ console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
292
+ });
293
+ console.log('');
294
+ }
295
+
296
+ return { isValid: errors.length === 0, errors, warnings };
297
+ }
298
+
299
+ _generateRouter(views) {
300
+ let routeMap = '';
301
+ views.forEach(v => {
302
+ const cap = v.name.charAt(0).toUpperCase() + v.name.slice(1);
303
+ if (v.name.toLowerCase() === 'index') routeMap += ` '/': render${cap},\n`;
304
+ routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
305
+ });
306
+
307
+ return `
308
+ // --- JUX SOURCE LOADER ---
309
+ var __juxSources = null;
310
+ async function __juxLoadSources() {
311
+ if (__juxSources) return __juxSources;
312
+ try {
313
+ var res = await fetch('/__jux_sources.json');
314
+ __juxSources = await res.json();
315
+ } catch (e) {
316
+ __juxSources = {};
317
+ }
318
+ return __juxSources;
319
+ }
320
+
321
+ // Find source file from error stack
322
+ function __juxFindSource(stack) {
323
+ var match = stack.match(/render(\\w+)/);
324
+ if (match) {
325
+ var viewName = match[1].toLowerCase();
326
+ for (var file in __juxSources || {}) {
327
+ if (__juxSources[file].name.toLowerCase() === viewName) {
328
+ return { file: file, source: __juxSources[file], viewName: match[1] };
329
+ }
330
+ }
331
+ }
332
+ return null;
333
+ }
334
+
335
+ // --- JUX RUNTIME ERROR OVERLAY ---
336
+ var __juxErrorOverlay = {
337
+ styles: \`
338
+ #__jux-error-overlay {
339
+ position: fixed; inset: 0; z-index: 99999;
340
+ background: rgba(0, 0, 0, 0.4);
341
+ display: flex; align-items: center; justify-content: center;
342
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
343
+ opacity: 0;
344
+ transition: opacity 0.2s ease-out;
345
+ backdrop-filter: blur(2px);
346
+ }
347
+ #__jux-error-overlay.visible { opacity: 1; }
348
+ #__jux-error-overlay * { box-sizing: border-box; }
349
+ .__jux-modal {
350
+ background: #f8f9fa;
351
+ border-radius: 4px;
352
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
353
+ max-width: 80vw;
354
+ width: 90%;
355
+ max-height: 90vh;
356
+ overflow: hidden;
357
+ display: flex;
358
+ flex-direction: column;
359
+ transform: translateY(10px);
360
+ transition: transform 0.2s ease-out;
361
+ }
362
+ #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
363
+ .__jux-header {
364
+ background: #fff;
365
+ padding: 20px 24px;
366
+ border-bottom: 1px solid #e5e7eb;
367
+ }
368
+ .__jux-header h3 {
369
+ margin: 0 0 6px; font-weight: 600; font-size: 11px;
370
+ color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px;
371
+ }
372
+ .__jux-header h1 {
373
+ margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626;
374
+ line-height: 1.3;
375
+ }
376
+ .__jux-header .file-info { color: #6b7280; font-size: 13px; }
377
+ .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
378
+ .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
379
+ .__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
380
+ .__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
381
+ .__jux-code-line.error { background: #fef2f2; }
382
+ .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
383
+ .__jux-code-line.context { background: #fefce8; }
384
+ .__jux-line-num {
385
+ min-width: 44px; padding: 4px 12px; text-align: right;
386
+ color: #9ca3af; background: #f9fafb; user-select: none;
387
+ border-right: 1px solid #e5e7eb; font-size: 12px;
388
+ }
389
+ .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
390
+ .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
391
+ .__jux-footer {
392
+ padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb;
393
+ display: flex; justify-content: space-between; align-items: center;
394
+ }
395
+ .__jux-tip { color: #6b7280; font-size: 12px; }
396
+ .__jux-dismiss {
397
+ background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
398
+ padding: 8px 16px; border-radius: 6px; cursor: pointer;
399
+ font-size: 13px; font-weight: 500; transition: background 0.15s;
400
+ }
401
+ .__jux-dismiss:hover { background: #e5e7eb; }
402
+ .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
403
+ .__jux-no-source pre {
404
+ background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px;
405
+ font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left;
406
+ border: 1px solid #e5e7eb;
407
+ }
408
+ \`,
409
+
410
+ show: async function(error, title) {
411
+ title = title || 'Runtime Error';
412
+
413
+ var existing = document.getElementById('__jux-error-overlay');
414
+ if (existing) existing.remove();
415
+
416
+ await __juxLoadSources();
417
+
418
+ var overlay = document.createElement('div');
419
+ overlay.id = '__jux-error-overlay';
420
+
421
+ var stack = error && error.stack ? error.stack : '';
422
+ var msg = error && error.message ? error.message : String(error);
423
+
424
+ var found = __juxFindSource(stack);
425
+ var sourceHtml = '';
426
+ var fileInfo = '';
427
+
428
+ if (found && found.source && found.source.lines) {
429
+ var lines = found.source.lines;
430
+ fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
431
+
432
+ var errorLineIndex = -1;
433
+ var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
434
+ if (errorMethod) {
435
+ for (var i = 0; i < lines.length; i++) {
436
+ if (lines[i].indexOf('.' + errorMethod[1]) > -1) {
437
+ errorLineIndex = i;
438
+ break;
439
+ }
440
+ }
441
+ }
442
+
443
+ if (errorLineIndex === -1) {
444
+ for (var i = 0; i < lines.length; i++) {
445
+ if (lines[i].indexOf('throw ') > -1 || lines[i].indexOf('throw(') > -1) {
446
+ errorLineIndex = i;
447
+ break;
448
+ }
449
+ }
450
+ }
451
+
452
+ var contextStart = Math.max(0, errorLineIndex - 3);
453
+ var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
454
+
455
+ if (errorLineIndex === -1) {
456
+ contextStart = 0;
457
+ contextEnd = Math.min(14, lines.length - 1);
458
+ }
459
+
460
+ for (var i = contextStart; i <= contextEnd; i++) {
461
+ var isError = (i === errorLineIndex);
462
+ var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
463
+ var lineClass = isError ? ' error' : (isContext ? ' context' : '');
464
+ var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
465
+ sourceHtml += '<div class="__jux-code-line' + lineClass + '">' +
466
+ '<span class="__jux-line-num">' + (i + 1) + '</span>' +
467
+ '<span class="__jux-line-code">' + lineCode + '</span>' +
468
+ '</div>';
469
+ }
470
+ } else {
471
+ sourceHtml = '<div class="__jux-no-source">' +
472
+ '<p>Could not locate source file for this error.</p>' +
473
+ '<pre>' + stack + '</pre>' +
474
+ '</div>';
475
+ }
476
+
477
+ overlay.innerHTML = '<style>' + this.styles + '</style>' +
478
+ '<div class="__jux-modal">' +
479
+ '<div class="__jux-header">' +
480
+ '<h3>' + title + '</h3>' +
481
+ '<h1>' + msg + '</h1>' +
482
+ fileInfo +
483
+ '</div>' +
484
+ '<div class="__jux-source">' +
485
+ '<div class="__jux-code">' + sourceHtml + '</div>' +
486
+ '</div>' +
487
+ '<div class="__jux-footer">' +
488
+ '<span class="__jux-tip">šŸ’” Fix the error and save to reload</span>' +
489
+ '<button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button>' +
490
+ '</div>' +
491
+ '</div>';
492
+
493
+ document.body.appendChild(overlay);
494
+
495
+ // Trigger transition
496
+ requestAnimationFrame(function() {
497
+ overlay.classList.add('visible');
498
+ });
499
+
500
+ console.error(title + ':', error);
501
+ }
502
+ };
503
+
504
+ // Global error handlers
505
+ window.addEventListener('error', function(e) {
506
+ __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error');
507
+ }, true);
508
+
509
+ window.addEventListener('unhandledrejection', function(e) {
510
+ __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection');
511
+ }, true);
512
+
513
+ // --- JUX ROUTER ---
514
+ const routes = {\n${routeMap}};
515
+
516
+ async function navigate(path) {
517
+ const view = routes[path];
518
+ if (!view) {
519
+ document.getElementById('app').innerHTML = '<h1 style="padding:40px;">404 - Not Found</h1>';
520
+ return;
521
+ }
522
+ document.getElementById('app').innerHTML = '';
523
+
524
+ var overlay = document.getElementById('__jux-error-overlay');
525
+ if (overlay) overlay.remove();
526
+
527
+ try {
528
+ await view();
529
+ } catch (err) {
530
+ __juxErrorOverlay.show(err, 'Jux Render Error');
531
+ }
532
+ }
533
+
534
+ document.addEventListener('click', e => {
535
+ const a = e.target.closest('a');
536
+ if (!a || a.dataset.router === 'false') return;
537
+ try { if (new URL(a.href, location.origin).origin !== location.origin) return; } catch { return; }
538
+ e.preventDefault();
539
+ history.pushState({}, '', a.href);
540
+ navigate(new URL(a.href, location.origin).pathname);
541
+ });
542
+
543
+ window.addEventListener('popstate', () => navigate(location.pathname));
544
+ navigate(location.pathname);
545
+ `;
546
+ }
547
+
548
+ async build() {
549
+ console.log('šŸš€ JUX Build\n');
550
+
551
+ const juxscriptPath = this.findJuxscriptPath();
552
+ if (!juxscriptPath) {
553
+ console.error('āŒ Could not locate juxscript package');
554
+ return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
555
+ }
556
+ console.log(`šŸ“¦ Using: ${juxscriptPath}`);
557
+
558
+ await this.loadJuxscriptExports();
559
+
560
+ if (fs.existsSync(this.distDir)) {
561
+ fs.rmSync(this.distDir, { recursive: true, force: true });
562
+ }
563
+ fs.mkdirSync(this.distDir, { recursive: true });
564
+
565
+ const { views, dataModules, sharedModules } = this.scanFiles();
566
+ console.log(`šŸ“ Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data\n`);
567
+
568
+ const cssFiles = this.copyJuxscriptCss();
569
+
570
+ // Copy data/shared modules to dist
571
+ const juxDistDir = path.join(this.distDir, 'jux');
572
+ fs.mkdirSync(juxDistDir, { recursive: true });
573
+ [...dataModules, ...sharedModules].forEach(m => {
574
+ fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
575
+ });
576
+
577
+ const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
578
+ const entryPath = path.join(this.distDir, 'entry.js');
579
+ fs.writeFileSync(entryPath, entryContent);
580
+
581
+ // Write source snapshot for dev tools
582
+ const snapshotPath = path.join(this.distDir, '__jux_sources.json');
583
+ fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
584
+ console.log(`šŸ“ø Source snapshot written`);
585
+
586
+ const validation = this.reportValidationIssues();
587
+ if (!validation.isValid) {
588
+ console.log('šŸ›‘ BUILD FAILED\n');
589
+ return { success: false, errors: validation.errors, warnings: validation.warnings };
590
+ }
591
+
592
+ try {
593
+ await esbuild.build({
594
+ entryPoints: [entryPath],
595
+ bundle: true,
596
+ outfile: path.join(this.distDir, 'bundle.js'),
597
+ format: 'esm',
598
+ platform: 'browser',
599
+ target: 'esnext',
600
+ sourcemap: true,
601
+ loader: { '.jux': 'js', '.css': 'empty' },
602
+ plugins: [{
603
+ name: 'juxscript-resolver',
604
+ setup: (build) => {
605
+ build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
606
+ }
607
+ }],
608
+ });
609
+ console.log('āœ… esbuild complete');
610
+ } catch (err) {
611
+ console.error('āŒ esbuild failed:', err);
612
+ return { success: false, errors: [{ message: err.message }], warnings: [] };
613
+ }
614
+
615
+ // Generate index.html
616
+ const html = `<!DOCTYPE html>
617
+ <html lang="en">
618
+ <head>
619
+ <meta charset="UTF-8">
620
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
621
+ <title>JUX App</title>
622
+ ${this.generateCssLinks(cssFiles)}
623
+ <script type="module" src="./bundle.js"></script>
624
+ </head>
625
+ <body>
626
+ <div id="app"></div>
627
+ </body>
628
+ </html>`;
629
+ fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
630
+
631
+ // Cleanup temp files
632
+ fs.unlinkSync(entryPath);
633
+ fs.rmSync(juxDistDir, { recursive: true, force: true });
634
+
635
+ console.log(`\nāœ… Build Complete!\n`);
636
+ return { success: true, errors: [], warnings: validation.warnings };
637
+ }
638
+ }