juxscript 1.1.0 → 1.1.3

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