juxscript 1.1.371 → 1.1.375

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/bin/cli.js CHANGED
@@ -67,6 +67,24 @@ function resolveDestFolderName(parentDir, baseName) {
67
67
  return `${baseName}-${Date.now()}`;
68
68
  }
69
69
 
70
+ function listFilesRecursive(dir, base = '') {
71
+ const results = [];
72
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
73
+
74
+ for (const entry of entries) {
75
+ if (entry.name.startsWith('.')) continue;
76
+ const rel = base ? path.join(base, entry.name) : entry.name;
77
+
78
+ if (entry.isDirectory()) {
79
+ results.push(...listFilesRecursive(path.join(dir, entry.name), rel));
80
+ } else {
81
+ results.push(rel);
82
+ }
83
+ }
84
+
85
+ return results;
86
+ }
87
+
70
88
  function promptPresetSelection(presets) {
71
89
  return new Promise((resolve) => {
72
90
  console.log('\nšŸ“¦ Available component presets:\n');
@@ -9,14 +9,23 @@ export const config = {
9
9
  directories: {
10
10
  source: './jux', // Where your .jux files live
11
11
  distribution: './.jux-dist', // Where build artifacts go
12
- public: './public' // āœ… Static assets folder (CSS, images, fonts, etc.)
12
+ public: './public' // Static assets folder (CSS, images, fonts, etc.)
13
13
  },
14
14
 
15
15
  // Application Defaults
16
16
  defaults: {
17
17
  httpPort: 3000,
18
18
  wsPort: 3001,
19
- //iconlibrary: 'heroicons'
19
+ },
20
+
21
+ // Precompile: bootstrap .jux files that run before routing.
22
+ // These render into sibling containers of #app at boot time.
23
+ // For CSS/JS injection, use jux.include() inside your .jux files.
24
+ //
25
+ // String form: 'sidebar/usage.jux' → auto-derives container id
26
+ // Object form: { file: 'sidebar/usage.jux', target: 'app-sidebar' }
27
+ precompile: {
28
+ juxfiles: [],
20
29
  }
21
30
 
22
31
  };
@@ -33,6 +33,10 @@ const defaults = {
33
33
  autoRoute: rawConfig.defaults?.autoRoute ?? true
34
34
  };
35
35
 
36
+ const precompile = {
37
+ juxfiles: rawConfig.precompile?.juxfiles || [],
38
+ };
39
+
36
40
  // Resolve absolute paths
37
41
  const paths = {
38
42
  source: path.resolve(PROJECT_ROOT, directories.source),
@@ -69,9 +73,10 @@ console.log(`\n`);
69
73
  const compiler = new JuxCompiler({
70
74
  srcDir: paths.source,
71
75
  distDir: paths.distribution,
72
- publicDir: directories.public, // āœ… Pass configured public directory name
76
+ publicDir: directories.public,
73
77
  defaults,
74
- paths // āœ… Pass resolved paths object (includes paths.public)
78
+ paths,
79
+ precompile
75
80
  });
76
81
 
77
82
  compiler.build()
@@ -18,6 +18,7 @@ export class JuxCompiler {
18
18
  this.publicDir = config.publicDir || './public';
19
19
  this.defaults = config.defaults || {};
20
20
  this.paths = config.paths || {};
21
+ this.precompile = config.precompile || { juxfiles: [] };
21
22
  this._juxscriptExports = null;
22
23
  this._juxscriptPath = null;
23
24
  this._renderFunctionCounter = 0;
@@ -28,7 +29,6 @@ export class JuxCompiler {
28
29
 
29
30
  const projectRoot = process.cwd();
30
31
 
31
- // 1. User's project node_modules (standard npm install)
32
32
  const userPath = path.resolve(projectRoot, 'node_modules/juxscript/dist/lib/index.js');
33
33
  if (fs.existsSync(userPath)) {
34
34
  console.log(`šŸ“¦ Using: ${userPath}`);
@@ -36,7 +36,6 @@ export class JuxCompiler {
36
36
  return userPath;
37
37
  }
38
38
 
39
- // 2. User's project node_modules — flat dist
40
39
  const userPathFlat = path.resolve(projectRoot, 'node_modules/juxscript/dist/index.js');
41
40
  if (fs.existsSync(userPathFlat)) {
42
41
  console.log(`šŸ“¦ Using: ${userPathFlat}`);
@@ -44,7 +43,6 @@ export class JuxCompiler {
44
43
  return userPathFlat;
45
44
  }
46
45
 
47
- // 3. Dev mode — built from parent package
48
46
  const packageRoot = path.resolve(__dirname, '..');
49
47
  const devPath = path.resolve(packageRoot, 'dist', 'lib', 'index.js');
50
48
  if (fs.existsSync(devPath)) {
@@ -53,7 +51,6 @@ export class JuxCompiler {
53
51
  return devPath;
54
52
  }
55
53
 
56
- // 4. Dev mode — flat dist
57
54
  const devPathFlat = path.resolve(packageRoot, 'dist', 'index.js');
58
55
  if (fs.existsSync(devPathFlat)) {
59
56
  console.log(`šŸ“¦ Using (dev flat): ${devPathFlat}`);
@@ -61,7 +58,6 @@ export class JuxCompiler {
61
58
  return devPathFlat;
62
59
  }
63
60
 
64
- // 5. Monorepo sibling
65
61
  const monoPath = path.resolve(projectRoot, '../jux/dist/lib/index.js');
66
62
  if (fs.existsSync(monoPath)) {
67
63
  console.log(`šŸ“¦ Using (mono): ${monoPath}`);
@@ -69,7 +65,6 @@ export class JuxCompiler {
69
65
  return monoPath;
70
66
  }
71
67
 
72
- // 6. Try require.resolve as last resort
73
68
  try {
74
69
  const resolved = require.resolve('juxscript');
75
70
  if (fs.existsSync(resolved)) {
@@ -79,7 +74,6 @@ export class JuxCompiler {
79
74
  }
80
75
  } catch (_) { }
81
76
 
82
- // Debug: show what was checked
83
77
  console.error('āŒ Searched for juxscript at:');
84
78
  console.error(` ${userPath}`);
85
79
  console.error(` ${userPathFlat}`);
@@ -87,7 +81,6 @@ export class JuxCompiler {
87
81
  console.error(` ${devPathFlat}`);
88
82
  console.error(` ${monoPath}`);
89
83
 
90
- // Check if node_modules/juxscript exists at all
91
84
  const juxscriptDir = path.resolve(projectRoot, 'node_modules/juxscript');
92
85
  if (fs.existsSync(juxscriptDir)) {
93
86
  console.error(`\nšŸ“ node_modules/juxscript exists. Contents:`);
@@ -137,7 +130,6 @@ export class JuxCompiler {
137
130
  } else if (hasExports) {
138
131
  sharedModules.push({ name, file: relativePath, content, originalContent: content });
139
132
  } else {
140
- // āœ… Auto-wrap pageState references before wrapping in async function
141
133
  let processedContent = content;
142
134
  if (file.endsWith('.jux')) {
143
135
  const result = autowrap(content, relativePath);
@@ -364,10 +356,8 @@ export class JuxCompiler {
364
356
  content: m.content,
365
357
  lines: m.content.split('\n')
366
358
  };
367
-
368
359
  let code = m.originalContent || m.content;
369
360
  code = this._stripImportsAndExports(code);
370
-
371
361
  entry += `\n// From: ${m.file}\n${code}\n`;
372
362
  });
373
363
 
@@ -379,20 +369,18 @@ export class JuxCompiler {
379
369
  content: m.content,
380
370
  lines: m.content.split('\n')
381
371
  };
382
-
383
372
  let code = m.originalContent || m.content;
384
373
  code = this._stripImportsAndExports(code);
385
-
386
374
  entry += `\n// From: ${m.file}\n${code}\n`;
387
375
  });
388
376
 
389
377
  entry += `\n// --- VIEW FUNCTIONS ---\n`;
390
378
 
391
379
  const routeToFunctionMap = new Map();
380
+ const fileToFunctionMap = new Map();
392
381
 
393
382
  views.forEach((v, index) => {
394
383
  const functionName = `renderJux${index}`;
395
-
396
384
  let codeBody = v.autowrappedContent || v.originalContent || v.content;
397
385
  const originalLines = (v.originalContent || codeBody).split('\n');
398
386
  codeBody = this._stripImportsAndExports(codeBody);
@@ -406,6 +394,9 @@ export class JuxCompiler {
406
394
 
407
395
  entry += `\nasync function ${functionName}() {\n${codeBody}\n}\n`;
408
396
 
397
+ const relNormalized = v.file.replace(/\\/g, '/');
398
+ fileToFunctionMap.set(relNormalized, functionName);
399
+
409
400
  const routePath = v.name
410
401
  .toLowerCase()
411
402
  .replace(/\\/g, '/')
@@ -427,12 +418,207 @@ export class JuxCompiler {
427
418
  this._sourceSnapshot = sourceSnapshot;
428
419
  this._validationIssues = [];
429
420
 
430
- // Embed source snapshot for runtime error overlay
431
421
  entry += `\nwindow.__juxSources = ${JSON.stringify(sourceSnapshot)};\n`;
432
422
 
423
+ const bootCalls = this._resolvePrecompileBootCalls(fileToFunctionMap);
424
+
425
+ entry += `\n// --- STARTUP ---\n`;
426
+ entry += `window.addEventListener('DOMContentLoaded', async () => {\n`;
427
+ for (const bc of bootCalls) {
428
+ entry += ` await ${bc.functionName}(); // boot: ${bc.file}\n`;
429
+ }
430
+ entry += ` route(window.location.pathname);\n`;
431
+ entry += `});\n`;
432
+
433
+ this._bootCalls = bootCalls;
434
+
433
435
  return entry;
434
436
  }
435
437
 
438
+ _resolvePrecompileBootCalls(fileToFunctionMap) {
439
+ const bootCalls = [];
440
+ const pc = this.precompile;
441
+
442
+ for (const jfEntry of pc.juxfiles) {
443
+ const jf = typeof jfEntry === 'string'
444
+ ? { file: jfEntry }
445
+ : jfEntry;
446
+
447
+ const fullPath = path.resolve(this.srcDir, jf.file);
448
+ const relativePath = path.relative(this.srcDir, fullPath).replace(/\\/g, '/');
449
+
450
+ const functionName = fileToFunctionMap.get(relativePath);
451
+ if (!functionName) {
452
+ console.error(`āŒ precompile.juxfiles: "${jf.file}" (${relativePath}) was not compiled as a view.`);
453
+ console.error(` It may have exports or be classified as a data/shared module.`);
454
+ console.error(` Available views: ${[...fileToFunctionMap.keys()].join(', ')}`);
455
+ continue;
456
+ }
457
+
458
+ let containerId = jf.target;
459
+ if (!containerId) {
460
+ const baseName = jf.file
461
+ .replace(/\.jux$/, '')
462
+ .replace(/\/index$/, '')
463
+ .replace(/[\/\\]/g, '-')
464
+ .replace(/[^a-z0-9-]/gi, '')
465
+ .replace(/-+/g, '-')
466
+ .replace(/^-|-$/g, '')
467
+ .toLowerCase();
468
+ containerId = 'app-' + (baseName || 'boot');
469
+ }
470
+
471
+ bootCalls.push({ file: relativePath, functionName, containerId });
472
+ console.log(` šŸ—ļø Boot: ${jf.file} → ${functionName} → <div id="${containerId}">`);
473
+ }
474
+
475
+ return bootCalls;
476
+ }
477
+
478
+ _generateRouter(routeToFunctionMap) {
479
+ let router = `\n// --- ROUTER ---\n`;
480
+ router += `const routes = {\n`;
481
+
482
+ routeToFunctionMap.forEach((functionName, route) => {
483
+ router += ` '${route}': ${functionName},\n`;
484
+ });
485
+
486
+ router += `};\n\n`;
487
+
488
+ const routeIndex = [];
489
+ routeToFunctionMap.forEach((functionName, routePath) => {
490
+ const name = routePath === '/'
491
+ ? 'Home'
492
+ : routePath.split('/').filter(Boolean).pop()
493
+ .split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
494
+ const sourceFile = this._sourceSnapshot
495
+ ? Object.values(this._sourceSnapshot).find(s => s.functionName === functionName)
496
+ : null;
497
+ routeIndex.push({ path: routePath, name, file: sourceFile?.file || '' });
498
+ });
499
+ router += `window.__juxRouteIndex = ${JSON.stringify(routeIndex)};\n\n`;
500
+
501
+ router += `function route(path) {
502
+ const renderFn = routes[path] || routes['/'];
503
+ if (renderFn) {
504
+ const app = document.getElementById('app');
505
+ if (app) app.innerHTML = '';
506
+
507
+ if (typeof renderFn === 'function') {
508
+ renderFn();
509
+ }
510
+ } else {
511
+ const app = document.getElementById('app');
512
+ if (app) app.innerHTML = '<h1>404 - Page Not Found</h1>';
513
+ }
514
+ }
515
+
516
+ window.addEventListener('popstate', () => route(window.location.pathname));
517
+
518
+ document.addEventListener('click', (e) => {
519
+ const link = e.target.closest('a[href]');
520
+ if (link) {
521
+ const href = link.getAttribute('href');
522
+ if (href.startsWith('/') && !href.startsWith('//')) {
523
+ e.preventDefault();
524
+ window.history.pushState({}, '', href);
525
+ route(href);
526
+ }
527
+ }
528
+ });
529
+
530
+ window.navigateTo = (path) => {
531
+ window.history.pushState({}, '', path);
532
+ route(path);
533
+ };
534
+ `;
535
+
536
+ return router;
537
+ }
538
+
539
+ _generateIndexHtml() {
540
+ const bootCalls = this._bootCalls || [];
541
+
542
+ let bodyContainers = '';
543
+ for (const bc of bootCalls) {
544
+ bodyContainers += ` <div id="${bc.containerId}"></div>\n`;
545
+ }
546
+
547
+ const html = `<!DOCTYPE html>
548
+ <html lang="en">
549
+ <head>
550
+ <meta charset="UTF-8">
551
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
552
+ <title>JUX Application</title>
553
+ ${generateErrorCollector({
554
+ enabled: process.env.NODE_ENV !== 'production',
555
+ maxErrors: 50
556
+ })}
557
+ <script type="module" src="/bundle.js"></script>
558
+ </head>
559
+ <body>
560
+ ${bodyContainers} <div id="app"></div>
561
+ </body>
562
+ </html>`;
563
+
564
+ return html;
565
+ }
566
+
567
+ copyPublicFolder() {
568
+ const publicSrc = this.paths?.public
569
+ ? this.paths.public
570
+ : path.resolve(process.cwd(), this.publicDir);
571
+
572
+ if (!fs.existsSync(publicSrc)) {
573
+ return;
574
+ }
575
+
576
+ console.log('šŸ“¦ Copying public assets...');
577
+
578
+ try {
579
+ this._copyDirRecursive(publicSrc, this.distDir, 0);
580
+ console.log('āœ… Public assets copied');
581
+ } catch (err) {
582
+ console.warn('āš ļø Error copying public folder:', err.message);
583
+ }
584
+ }
585
+
586
+ _copyDirRecursive(src, dest, depth = 0) {
587
+ const entries = fs.readdirSync(src, { withFileTypes: true });
588
+
589
+ entries.forEach(entry => {
590
+ if (entry.name.startsWith('.')) return;
591
+
592
+ const srcPath = path.join(src, entry.name);
593
+ const destPath = path.join(dest, entry.name);
594
+
595
+ if (entry.isDirectory()) {
596
+ if (!fs.existsSync(destPath)) {
597
+ fs.mkdirSync(destPath, { recursive: true });
598
+ }
599
+ this._copyDirRecursive(srcPath, destPath, depth + 1);
600
+ } else {
601
+ fs.copyFileSync(srcPath, destPath);
602
+
603
+ if (depth === 0) {
604
+ const ext = path.extname(entry.name);
605
+ const icon = this._getFileIcon(ext);
606
+ console.log(` ${icon} ${entry.name}`);
607
+ }
608
+ }
609
+ });
610
+ }
611
+
612
+ _getFileIcon(ext) {
613
+ const icons = {
614
+ '.html': 'šŸ“„', '.css': 'šŸŽØ', '.js': 'šŸ“œ', '.json': 'šŸ“‹',
615
+ '.png': 'šŸ–¼ļø', '.jpg': 'šŸ–¼ļø', '.jpeg': 'šŸ–¼ļø', '.gif': 'šŸ–¼ļø',
616
+ '.svg': 'šŸŽØ', '.ico': 'šŸ”–',
617
+ '.woff': 'šŸ”¤', '.woff2': 'šŸ”¤', '.ttf': 'šŸ”¤', '.eot': 'šŸ”¤'
618
+ };
619
+ return icons[ext.toLowerCase()] || 'šŸ“¦';
620
+ }
621
+
436
622
  _stripImportsAndExports(code) {
437
623
  try {
438
624
  const ast = acorn.parse(code, {
@@ -455,31 +641,89 @@ export class JuxCompiler {
455
641
  let result = code;
456
642
  let offset = 0;
457
643
 
458
- nodesToRemove.forEach(node => {
459
- let start = node.start - offset;
460
- let end = node.end - offset;
644
+ for (const node of nodesToRemove) {
645
+ const start = node.start - offset;
646
+ const end = node.end - offset;
461
647
 
462
648
  if (node.type === 'ExportNamedDeclaration' && node.declaration) {
463
- const declarationStart = node.declaration.start - offset;
464
- result = result.substring(0, start) + result.substring(declarationStart);
465
- offset += (declarationStart - start);
649
+ const declStart = node.declaration.start - offset;
650
+ result = result.substring(0, start) + result.substring(declStart);
651
+ offset += (declStart - start);
466
652
  } else if (node.type === 'ExportDefaultDeclaration') {
467
- const exportKeyword = result.substring(start, end).match(/export\s+default\s+/);
468
- if (exportKeyword) {
469
- result = result.substring(0, start) + result.substring(start + exportKeyword[0].length);
470
- offset += exportKeyword[0].length;
653
+ const match = result.substring(start, end).match(/export\s+default\s+/);
654
+ if (match) {
655
+ result = result.substring(0, start) + result.substring(start + match[0].length);
656
+ offset += match[0].length;
471
657
  }
472
658
  } else {
473
659
  result = result.substring(0, start) + result.substring(end);
474
660
  offset += (end - start);
475
661
  }
476
- });
662
+ }
477
663
 
478
664
  return result.trim();
479
665
  } catch (parseError) {
480
- console.warn(`āš ļø Could not parse code for stripping, keeping as-is`);
481
- return code.trim();
666
+ return code
667
+ .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
668
+ .replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
669
+ .replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
670
+ .replace(/^\s*import\s*;?\s*$/gm, '')
671
+ .trim();
672
+ }
673
+ }
674
+
675
+ _validatePrecompile() {
676
+ const errors = [];
677
+ const pc = this.precompile;
678
+
679
+ for (const jfEntry of pc.juxfiles) {
680
+ const jf = typeof jfEntry === 'string' ? jfEntry : jfEntry.file;
681
+ const fullPath = path.resolve(this.srcDir, jf);
682
+ if (!fs.existsSync(fullPath)) {
683
+ errors.push({ message: `precompile.juxfiles: "${jf}" not found at ${fullPath}` });
684
+ }
685
+ }
686
+
687
+ if (errors.length > 0) {
688
+ console.error('\nāŒ Precompile validation failed:\n');
689
+ errors.forEach(e => console.error(` ${e.message}`));
690
+ } else if (pc.juxfiles.length) {
691
+ console.log(`\nšŸ”§ Precompile:`);
692
+ console.log(` šŸ“„ juxfiles: ${pc.juxfiles.map(j => typeof j === 'string' ? j : j.file).join(', ')}`);
693
+ }
694
+
695
+ return { valid: errors.length === 0, errors };
696
+ }
697
+
698
+ reportValidationIssues() {
699
+ const errors = [];
700
+ const warnings = [];
701
+
702
+ if (this._validationIssues && this._validationIssues.length > 0) {
703
+ this._validationIssues.forEach(issue => {
704
+ if (issue.type === 'error') {
705
+ errors.push(issue);
706
+ } else if (issue.type === 'warning') {
707
+ warnings.push(issue);
708
+ }
709
+ });
710
+ }
711
+
712
+ if (warnings.length > 0) {
713
+ console.log('\nāš ļø Warnings:\n');
714
+ warnings.forEach(w => {
715
+ console.log(` ${w.view}:${w.line} - ${w.message}`);
716
+ });
717
+ }
718
+
719
+ if (errors.length > 0) {
720
+ console.log('\nāŒ Errors:\n');
721
+ errors.forEach(e => {
722
+ console.log(` ${e.view}:${e.line} - ${e.message}`);
723
+ });
482
724
  }
725
+
726
+ return { isValid: errors.length === 0, errors, warnings };
483
727
  }
484
728
 
485
729
  async build() {
@@ -502,6 +746,12 @@ export class JuxCompiler {
502
746
 
503
747
  this.copyPublicFolder();
504
748
 
749
+ const precompileResult = this._validatePrecompile();
750
+ if (!precompileResult.valid) {
751
+ console.log('šŸ›‘ BUILD FAILED — precompile errors\n');
752
+ return { success: false, errors: precompileResult.errors, warnings: [] };
753
+ }
754
+
505
755
  const { views, dataModules, sharedModules } = this.scanFiles();
506
756
  console.log(`šŸ“ Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
507
757
 
@@ -531,271 +781,63 @@ export class JuxCompiler {
531
781
  platform: 'browser',
532
782
  target: 'esnext',
533
783
  sourcemap: true,
534
-
535
- loader: {
536
- '.jux': 'js',
537
- '.css': 'empty'
538
- },
539
-
784
+ loader: { '.jux': 'js', '.css': 'empty' },
540
785
  plugins: [{
541
786
  name: 'juxscript-resolver',
542
787
  setup: (build) => {
543
788
  build.onResolve({ filter: /^juxscript$/ }, () => ({
544
789
  path: juxscriptPath
545
790
  }));
546
-
547
791
  build.onResolve({ filter: /^axios$/ }, () => {
548
792
  const projectRoot = process.cwd();
549
793
  const axiosPath = path.resolve(projectRoot, 'node_modules/axios/dist/esm/axios.js');
550
-
551
794
  if (fs.existsSync(axiosPath)) {
552
795
  console.log('āœ… Found axios at:', axiosPath);
553
796
  return { path: axiosPath };
554
797
  }
555
-
556
798
  console.error('āŒ axios not found in project node_modules');
557
799
  return null;
558
800
  });
559
-
560
801
  build.onResolve({ filter: /\.jux$/ }, (args) => {
561
802
  console.log(`šŸ” Resolving: ${args.path} from ${args.importer}`);
562
-
563
- if (path.isAbsolute(args.path)) {
564
- return { path: args.path };
565
- }
566
-
803
+ if (path.isAbsolute(args.path)) return { path: args.path };
567
804
  if (args.path.startsWith('.')) {
568
805
  const importer = args.importer || entryPath;
569
806
  const importerDir = path.dirname(importer);
570
807
  const resolvedPath = path.resolve(importerDir, args.path);
571
-
572
- if (fs.existsSync(resolvedPath)) {
573
- return { path: resolvedPath };
574
- } else {
575
- console.error(`āŒ Could not resolve ${args.path} from ${importer}`);
576
- }
808
+ if (fs.existsSync(resolvedPath)) return { path: resolvedPath };
809
+ else console.error(`āŒ Could not resolve ${args.path} from ${importer}`);
577
810
  }
578
-
579
811
  return null;
580
812
  });
581
813
  }
582
814
  }],
583
-
584
815
  mainFields: ['browser', 'module', 'main'],
585
816
  conditions: ['browser', 'import', 'module', 'default'],
586
-
587
817
  define: {
588
818
  'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
589
819
  'global': 'globalThis',
590
820
  'process.env': JSON.stringify({})
591
821
  },
592
-
593
822
  minify: false,
594
823
  treeShaking: true,
595
824
  metafile: true,
596
825
  });
597
826
 
598
827
  console.log('\nāœ… Build complete');
599
-
600
828
  const bundlePath = path.join(this.distDir, 'bundle.js');
601
829
  const bundleStats = fs.statSync(bundlePath);
602
- const bundleSizeKB = (bundleStats.size / 1024).toFixed(2);
603
- console.log(`šŸ“¦ Bundle size: ${bundleSizeKB} KB`);
830
+ console.log(`šŸ“¦ Bundle size: ${(bundleStats.size / 1024).toFixed(2)} KB`);
604
831
 
605
832
  } catch (err) {
606
833
  console.error('āŒ esbuild failed:', err);
607
834
  return { success: false, errors: [{ message: err.message }], warnings: [] };
608
835
  }
609
836
 
610
- const html = `<!DOCTYPE html>
611
- <html lang="en">
612
- <head>
613
- <meta charset="UTF-8">
614
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
615
- <title>JUX Application</title>
616
- ${generateErrorCollector({
617
- enabled: process.env.NODE_ENV !== 'production',
618
- maxErrors: 50
619
- })}
620
- <script type="module" src="/bundle.js"></script>
621
- </head>
622
- <body>
623
- <div id="app"></div>
624
- </body>
625
- </html>`;
626
-
627
- fs.writeFileSync(
628
- path.join(this.config.distDir, 'index.html'),
629
- html,
630
- 'utf8'
631
- );
837
+ const html = this._generateIndexHtml();
838
+ fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
632
839
 
633
840
  console.log('āœ… Generated index.html\n');
634
841
  return { success: true, errors: [], warnings: validation.warnings };
635
842
  }
636
-
637
- _generateRouter(routeToFunctionMap) {
638
- let router = `\n// --- ROUTER ---\n`;
639
- router += `const routes = {\n`;
640
-
641
- routeToFunctionMap.forEach((functionName, route) => {
642
- router += ` '${route}': ${functionName},\n`;
643
- });
644
-
645
- router += `};\n\n`;
646
-
647
- // Build route index for jux.routes.all()
648
- const routeIndex = [];
649
- routeToFunctionMap.forEach((functionName, routePath) => {
650
- const name = routePath === '/'
651
- ? 'Home'
652
- : routePath.split('/').filter(Boolean).pop()
653
- .split('-').map(s => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
654
- // Find matching source file from snapshot
655
- const sourceFile = this._sourceSnapshot
656
- ? Object.values(this._sourceSnapshot).find(s => s.functionName === functionName)
657
- : null;
658
- routeIndex.push({ path: routePath, name, file: sourceFile?.file || '' });
659
- });
660
- router += `window.__juxRouteIndex = ${JSON.stringify(routeIndex)};\n\n`;
661
-
662
- router += `function route(path) {
663
- const renderFn = routes[path] || routes['/'];
664
- if (renderFn) {
665
- const appMain = document.getElementById('appmain-content');
666
- if (appMain) appMain.innerHTML = '';
667
-
668
- const app = document.getElementById('app');
669
- if (app) app.innerHTML = '';
670
-
671
- if (typeof renderFn === 'function') {
672
- renderFn();
673
- }
674
- } else {
675
- const app = document.getElementById('app');
676
- if (app) app.innerHTML = '<h1>404 - Page Not Found</h1>';
677
- }
678
- }
679
-
680
- window.addEventListener('DOMContentLoaded', () => {
681
- route(window.location.pathname);
682
- });
683
-
684
- window.addEventListener('popstate', () => route(window.location.pathname));
685
-
686
- document.addEventListener('click', (e) => {
687
- const link = e.target.closest('a[href]');
688
- if (link) {
689
- const href = link.getAttribute('href');
690
- if (href.startsWith('/') && !href.startsWith('//')) {
691
- e.preventDefault();
692
- window.history.pushState({}, '', href);
693
- route(href);
694
- }
695
- }
696
- });
697
-
698
- window.navigateTo = (path) => {
699
- window.history.pushState({}, '', path);
700
- route(path);
701
- };
702
- `;
703
-
704
- return router;
705
- }
706
-
707
- reportValidationIssues() {
708
- const errors = [];
709
- const warnings = [];
710
-
711
- if (this._validationIssues && this._validationIssues.length > 0) {
712
- this._validationIssues.forEach(issue => {
713
- if (issue.type === 'error') {
714
- errors.push(issue);
715
- } else if (issue.type === 'warning') {
716
- warnings.push(issue);
717
- }
718
- });
719
- }
720
-
721
- if (warnings.length > 0) {
722
- console.log('\nāš ļø Warnings:\n');
723
- warnings.forEach(w => {
724
- console.log(` ${w.view}:${w.line} - ${w.message}`);
725
- });
726
- }
727
-
728
- if (errors.length > 0) {
729
- console.log('\nāŒ Errors:\n');
730
- errors.forEach(e => {
731
- console.log(` ${e.view}:${e.line} - ${e.message}`);
732
- });
733
- }
734
-
735
- return { isValid: errors.length === 0, errors, warnings };
736
- }
737
-
738
- copyPublicFolder() {
739
- const publicSrc = this.paths.public
740
- ? this.paths.public
741
- : path.resolve(process.cwd(), this.publicDir);
742
-
743
- if (!fs.existsSync(publicSrc)) {
744
- return;
745
- }
746
-
747
- console.log('šŸ“¦ Copying public assets...');
748
-
749
- try {
750
- this._copyDirRecursive(publicSrc, this.distDir, 0);
751
- console.log('āœ… Public assets copied');
752
- } catch (err) {
753
- console.warn('āš ļø Error copying public folder:', err.message);
754
- }
755
- }
756
-
757
- _copyDirRecursive(src, dest, depth = 0) {
758
- const entries = fs.readdirSync(src, { withFileTypes: true });
759
-
760
- entries.forEach(entry => {
761
- if (entry.name.startsWith('.')) return;
762
-
763
- const srcPath = path.join(src, entry.name);
764
- const destPath = path.join(dest, entry.name);
765
-
766
- if (entry.isDirectory()) {
767
- if (!fs.existsSync(destPath)) {
768
- fs.mkdirSync(destPath, { recursive: true });
769
- }
770
- this._copyDirRecursive(srcPath, destPath, depth + 1);
771
- } else {
772
- fs.copyFileSync(srcPath, destPath);
773
-
774
- if (depth === 0) {
775
- const ext = path.extname(entry.name);
776
- const icon = this._getFileIcon(ext);
777
- console.log(` ${icon} ${entry.name}`);
778
- }
779
- }
780
- });
781
- }
782
-
783
- _getFileIcon(ext) {
784
- const icons = {
785
- '.html': 'šŸ“„', '.css': 'šŸŽØ', '.js': 'šŸ“œ', '.json': 'šŸ“‹',
786
- '.png': 'šŸ–¼ļø', '.jpg': 'šŸ–¼ļø', '.jpeg': 'šŸ–¼ļø', '.gif': 'šŸ–¼ļø',
787
- '.svg': 'šŸŽØ', '.ico': 'šŸ”–',
788
- '.woff': 'šŸ”¤', '.woff2': 'šŸ”¤', '.ttf': 'šŸ”¤', '.eot': 'šŸ”¤'
789
- };
790
- return icons[ext.toLowerCase()] || 'šŸ“¦';
791
- }
792
-
793
- _generateNameFromPath(filepath) {
794
- return filepath
795
- .replace(/\.jux$/, '')
796
- .replace(/[\/\\]/g, '_')
797
- .replace(/[^a-zA-Z0-9_]/g, '_')
798
- .replace(/_+/g, '_')
799
- .replace(/^_|_$/g, '');
800
- }
801
843
  }
@@ -29,11 +29,11 @@ function getArgValue(flag, shortFlag, defaultValue) {
29
29
  const PORT = parseInt(getArgValue('--port', '-p', process.env.PORT || '3000'));
30
30
  const WS_PORT = parseInt(getArgValue('--ws-port', '-w', process.env.WS_PORT || String(PORT + 1)));
31
31
 
32
- // āœ… Load juxconfig.js to get configured paths
32
+ // Where the config is loaded (near the top, after imports)
33
33
  const PROJECT_ROOT = process.cwd();
34
34
  const JUX_CONFIG_PATH = path.resolve(PROJECT_ROOT, 'juxconfig.js');
35
-
36
35
  let rawConfig = {};
36
+
37
37
  try {
38
38
  const imported = await import(JUX_CONFIG_PATH);
39
39
  rawConfig = imported.config || {};
@@ -70,8 +70,9 @@ if (!fs.existsSync(DIST_DIR) || !fs.existsSync(path.join(DIST_DIR, 'index.html')
70
70
  const compiler = new JuxCompiler({
71
71
  srcDir: SRC_DIR,
72
72
  distDir: DIST_DIR,
73
- publicDir: directories.public, // āœ… Pass configured name
74
- paths // āœ… Pass resolved paths
73
+ publicDir: directories.public,
74
+ paths,
75
+ precompile
75
76
  });
76
77
 
77
78
  try {
@@ -106,6 +107,11 @@ app.use((req, res, next) => {
106
107
  app.get('/favicon.ico', (req, res) => res.status(204).end());
107
108
  app.get('/favicon.png', (req, res) => res.status(204).end());
108
109
 
110
+ // Near the top, after config is loaded — ensure precompile is extracted:
111
+ const precompile = {
112
+ juxfiles: rawConfig.precompile?.juxfiles || [],
113
+ };
114
+
109
115
  // ═══════════════════════════════════════════════════════════════
110
116
  // API ROUTES — BEFORE static and catch-all
111
117
  // ═══════════════════════════════════════════════════════════════
@@ -276,8 +282,9 @@ if (HOT_RELOAD) {
276
282
  const compiler = new JuxCompiler({
277
283
  srcDir: SRC_DIR,
278
284
  distDir: DIST_DIR,
279
- publicDir: directories.public, // āœ… Pass configured name
280
- paths // āœ… Pass resolved paths
285
+ publicDir: directories.public,
286
+ paths,
287
+ precompile
281
288
  });
282
289
 
283
290
  watcher = createWatcher(SRC_DIR, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.371",
3
+ "version": "1.1.375",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "./dist/lib/index.js",
@@ -28,13 +28,14 @@ export function juxSidebar(id, sections, options) {
28
28
  if (typeof item === 'string') {
29
29
  var path = item.startsWith('/') ? item : '/' + item;
30
30
  var seg = path.split('/').filter(Boolean).pop() || 'Home';
31
- return { id: id + '-' + seg.toLowerCase().replace(/[^a-z0-9]/g, '-'), label: seg.charAt(0).toUpperCase() + seg.slice(1), path: path };
31
+ return { id: id + '-' + path.replace(/^\//, '').replace(/[^a-z0-9]/gi, '-').toLowerCase() || id + '-home', label: seg.charAt(0).toUpperCase() + seg.slice(1), path: path };
32
32
  }
33
33
  // Support RouteInfo shape { path, name, file } from jux.routes.all()
34
34
  var itemPath = item.path || '';
35
35
  var seg = itemPath.split('/').filter(Boolean).pop() || 'home';
36
+ var fullSlug = itemPath.replace(/^\//, '').replace(/[^a-z0-9]/gi, '-').toLowerCase() || 'home';
36
37
  return {
37
- id: item.id || id + '-' + seg.toLowerCase().replace(/[^a-z0-9]/g, '-'),
38
+ id: item.id || id + '-' + fullSlug,
38
39
  label: item.label || item.name || seg.charAt(0).toUpperCase() + seg.slice(1),
39
40
  path: itemPath,
40
41
  icon: item.icon
@@ -57,11 +58,34 @@ export function juxSidebar(id, sections, options) {
57
58
  injectStyles();
58
59
  var sidebarClass = 'jux-sidebar jux-sidebar--' + _density;
59
60
  if (_collapsed) sidebarClass += ' jux-sidebar--collapsed';
60
- jux.div(id, { class: sidebarClass, style: 'width:' + (_collapsed ? _collapsedWidth : _width), target: _target });
61
+
62
+ // If the target container already has our ID, reuse it instead of nesting
63
+ var existingEl = document.getElementById(id);
64
+ if (_target === id && existingEl) {
65
+ existingEl.className = sidebarClass;
66
+ existingEl.style.width = _collapsed ? _collapsedWidth : _width;
67
+ } else {
68
+ jux.div(id, { class: sidebarClass, style: 'width:' + (_collapsed ? _collapsedWidth : _width), target: _target });
69
+ }
70
+
61
71
  jux.div(id + '-header', { class: 'jux-sidebar-header', target: id });
62
- jux.div(id + '-logo', { class: 'jux-sidebar-logo', target: id + '-header' });
72
+ jux.div(id + '-header-link', { class: 'jux-sidebar-header-link', target: id + '-header' });
73
+ jux.div(id + '-logo', { class: 'jux-sidebar-logo', target: id + '-header-link' });
63
74
  renderLogo();
64
- jux.span(id + '-title', { class: 'jux-sidebar-title-text', content: _title, target: id + '-header' });
75
+ jux.span(id + '-title', { class: 'jux-sidebar-title-text', content: _title, target: id + '-header-link' });
76
+
77
+ // Make header link navigate to root
78
+ var headerLinkEl = document.getElementById(id + '-header-link');
79
+ if (headerLinkEl) {
80
+ headerLinkEl.addEventListener('click', function () {
81
+ if (window.navigateTo) {
82
+ window.navigateTo('/');
83
+ } else {
84
+ window.location.href = '/';
85
+ }
86
+ });
87
+ }
88
+
65
89
  if (_collapsible) {
66
90
  jux.div(id + '-collapse-btn', {
67
91
  class: 'jux-sidebar-collapse-btn',
@@ -116,10 +140,19 @@ export function juxSidebar(id, sections, options) {
116
140
  }
117
141
 
118
142
  function injectStyles() {
119
- jux.style('menu-styles', '\
143
+ var appPadding = {
144
+ dense: '10px',
145
+ normal: '16px',
146
+ inflated: '24px'
147
+ };
148
+ var currentWidth = _collapsed ? _collapsedWidth : _width;
149
+ jux.style('sidebar-styles', '\
120
150
  body { margin: 0; }\
151
+ #app { left: '+ currentWidth + '; position: fixed; top: 0; width: calc(100% - ' + currentWidth + '); height: 100vh; padding: ' + appPadding[_density] + '; box-sizing: border-box; transition: left 0.2s ease, width 0.2s ease, padding 0.2s ease; overflow-y: auto; }\
121
152
  .jux-sidebar { display:flex; flex-direction:column; min-height:100vh; background:hsl(var(--card)); color:hsl(var(--card-foreground)); border-right:1px solid hsl(var(--border)); font-family:var(--font-sans,system-ui,sans-serif); transition:width 0.2s ease; overflow:hidden; }\
122
153
  .jux-sidebar-header { display:flex; align-items:center; gap:8px; font-size:14px; font-weight:600; border-bottom:1px solid hsl(var(--border)); min-height:20px; position:relative; }\
154
+ .jux-sidebar-header-link { display:flex; align-items:center; gap:8px; cursor:pointer; flex:1; min-width:0; text-decoration:none; color:inherit; }\
155
+ .jux-sidebar-header-link:hover { opacity:0.8; }\
123
156
  .jux-sidebar-logo { width:20px; height:20px; min-width:20px; border-radius:6px; background:hsl(var(--primary)); display:flex; align-items:center; justify-content:center; color:hsl(var(--primary-foreground)); font-size:11px; font-weight:700; flex-shrink:0; overflow:hidden; }\
124
157
  .jux-sidebar-logo-img { width:100%; height:100%; object-fit:cover; border-radius:inherit; }\
125
158
  .jux-sidebar-logo svg { width:14px; height:14px; }\
@@ -146,24 +179,28 @@ export function juxSidebar(id, sections, options) {
146
179
  .jux-sidebar--collapsed .jux-sidebar-collapse-btn { margin-left:0; width:24px; height:24px; }\
147
180
  .jux-sidebar--collapsed .jux-sidebar-collapse-btn svg { width:14px; height:14px; }\
148
181
  .jux-sidebar--collapsed .jux-sidebar-logo { margin:0; }\
182
+ .jux-sidebar--collapsed ~ #app, .jux-sidebar--collapsed + #app { left: ' + _collapsedWidth + '; width: calc(100% - ' + _collapsedWidth + '); }\
149
183
  \
150
184
  .jux-sidebar--dense .jux-sidebar-header { padding:' + DENSITY.dense.headerPad + '; }\
151
185
  .jux-sidebar--dense .jux-sidebar-label { padding:' + DENSITY.dense.labelPad + '; }\
152
186
  .jux-sidebar--dense .jux-sidebar-footer { padding:' + DENSITY.dense.footerPad + '; }\
153
187
  .jux-sidebar--dense .jux-nav-item { padding:' + DENSITY.dense.itemPad + '; margin:' + DENSITY.dense.gap + ' 0; font-size:' + DENSITY.dense.fontSize + '; gap:8px; }\
154
188
  .jux-sidebar--dense .jux-nav-item-icon { width:16px; font-size:' + DENSITY.dense.iconSize + '; }\
189
+ .jux-sidebar--dense ~ #app, .jux-sidebar--dense + #app { padding: ' + appPadding.dense + '; }\
155
190
  \
156
191
  .jux-sidebar--normal .jux-sidebar-header { padding:' + DENSITY.normal.headerPad + '; }\
157
192
  .jux-sidebar--normal .jux-sidebar-label { padding:' + DENSITY.normal.labelPad + '; }\
158
193
  .jux-sidebar--normal .jux-sidebar-footer { padding:' + DENSITY.normal.footerPad + '; }\
159
194
  .jux-sidebar--normal .jux-nav-item { padding:' + DENSITY.normal.itemPad + '; margin:' + DENSITY.normal.gap + ' 0; font-size:' + DENSITY.normal.fontSize + '; gap:10px; }\
160
195
  .jux-sidebar--normal .jux-nav-item-icon { width:18px; font-size:' + DENSITY.normal.iconSize + '; }\
196
+ .jux-sidebar--normal ~ #app, .jux-sidebar--normal + #app { padding: ' + appPadding.normal + '; }\
161
197
  \
162
198
  .jux-sidebar--inflated .jux-sidebar-header { padding:' + DENSITY.inflated.headerPad + '; }\
163
199
  .jux-sidebar--inflated .jux-sidebar-label { padding:' + DENSITY.inflated.labelPad + '; }\
164
200
  .jux-sidebar--inflated .jux-sidebar-footer { padding:' + DENSITY.inflated.footerPad + '; }\
165
201
  .jux-sidebar--inflated .jux-nav-item { padding:' + DENSITY.inflated.itemPad + '; margin:' + DENSITY.inflated.gap + ' 0; font-size:' + DENSITY.inflated.fontSize + '; gap:12px; }\
166
202
  .jux-sidebar--inflated .jux-nav-item-icon { width:20px; font-size:' + DENSITY.inflated.iconSize + '; }\
203
+ .jux-sidebar--inflated ~ #app, .jux-sidebar--inflated + #app { padding: ' + appPadding.inflated + '; }\
167
204
  ');
168
205
  }
169
206
 
@@ -1,7 +1,7 @@
1
1
  import { jux } from 'juxscript';
2
2
  import { juxSidebar } from './index.jux';
3
3
  const allRoutes = jux.routes.all();
4
-
4
+ // ['/route/path', '/another/route']
5
5
  juxSidebar('my-sidebar', allRoutes,
6
6
  { title: 'Simple App', logo: 'J', footer: 'v1.0', density: 'inflated', collapsible: true });
7
7