juxscript 1.1.157 → 1.1.158

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.
@@ -14,30 +14,25 @@ export class JuxCompiler {
14
14
  this.config = config;
15
15
  this.srcDir = config.srcDir || './jux';
16
16
  this.distDir = config.distDir || './.jux-dist';
17
- this.publicDir = config.publicDir || './public'; // ✅ Configurable public path
17
+ this.publicDir = config.publicDir || './public';
18
18
  this.defaults = config.defaults || {};
19
19
  this.paths = config.paths || {};
20
20
  this._juxscriptExports = null;
21
21
  this._juxscriptPath = null;
22
- this._renderFunctionCounter = 0; // ✅ Add counter for unique IDs
22
+ this._renderFunctionCounter = 0;
23
23
  }
24
24
 
25
- /**
26
- * Locate juxscript package - simplified resolution
27
- */
28
25
  findJuxscriptPath() {
29
26
  if (this._juxscriptPath) return this._juxscriptPath;
30
27
 
31
28
  const projectRoot = process.cwd();
32
29
 
33
- // Priority 1: User's node_modules (when used as dependency)
34
30
  const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
35
31
  if (fs.existsSync(userPath)) {
36
32
  this._juxscriptPath = userPath;
37
33
  return userPath;
38
34
  }
39
35
 
40
- // Priority 2: Package root (when developing juxscript itself)
41
36
  const packageRoot = path.resolve(__dirname, '..');
42
37
  const devPath = path.resolve(packageRoot, 'index.js');
43
38
  if (fs.existsSync(devPath)) {
@@ -45,7 +40,6 @@ export class JuxCompiler {
45
40
  return devPath;
46
41
  }
47
42
 
48
- // Priority 3: Sibling in monorepo
49
43
  const monoPath = path.resolve(projectRoot, '../jux/index.js');
50
44
  if (fs.existsSync(monoPath)) {
51
45
  this._juxscriptPath = monoPath;
@@ -55,9 +49,6 @@ export class JuxCompiler {
55
49
  return null;
56
50
  }
57
51
 
58
- /**
59
- * ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
60
- */
61
52
  scanFiles() {
62
53
  const views = [], dataModules = [], sharedModules = [];
63
54
 
@@ -77,26 +68,13 @@ export class JuxCompiler {
77
68
  const relativePath = path.relative(this.srcDir, fullPath);
78
69
  const name = relativePath.replace(/\.[^/.]+$/, '');
79
70
 
80
- // Check if it has exports (module) or is executable code (view)
81
71
  const hasExports = /export\s+(default|const|function|class|{)/.test(content);
82
72
 
83
73
  if (file.includes('data')) {
84
- dataModules.push({
85
- name,
86
- file: relativePath,
87
- content,
88
- originalContent: content
89
- });
74
+ dataModules.push({ name, file: relativePath, content, originalContent: content });
90
75
  } else if (hasExports) {
91
- // Any file with exports is a module (not just .js files)
92
- sharedModules.push({
93
- name,
94
- file: relativePath,
95
- content,
96
- originalContent: content
97
- });
76
+ sharedModules.push({ name, file: relativePath, content, originalContent: content });
98
77
  } else {
99
- // .jux files without exports are views - use AST to extract imports
100
78
  let wrappedContent;
101
79
  try {
102
80
  const ast = acorn.parse(content, {
@@ -108,7 +86,6 @@ export class JuxCompiler {
108
86
  const imports = [];
109
87
  let lastImportEnd = 0;
110
88
 
111
- // Collect imports and track where they end
112
89
  for (const node of ast.body) {
113
90
  if (node.type === 'ImportDeclaration') {
114
91
  imports.push(content.substring(node.start, node.end));
@@ -116,10 +93,8 @@ export class JuxCompiler {
116
93
  }
117
94
  }
118
95
 
119
- // Get the rest of the code (everything after imports)
120
96
  const restOfCode = content.substring(lastImportEnd).trim();
121
97
 
122
- // Build wrapped content
123
98
  wrappedContent = [
124
99
  ...imports,
125
100
  '',
@@ -129,17 +104,11 @@ export class JuxCompiler {
129
104
  ].join('\n');
130
105
 
131
106
  } catch (parseError) {
132
- // Fallback: if parsing fails, just wrap the whole thing
133
107
  console.warn(`⚠️ Could not parse ${relativePath}, using basic wrapping`);
134
108
  wrappedContent = `export default async function() {\n${content}\n}`;
135
109
  }
136
110
 
137
- views.push({
138
- name,
139
- file: relativePath,
140
- content: wrappedContent,
141
- originalContent: content
142
- });
111
+ views.push({ name, file: relativePath, content: wrappedContent, originalContent: content });
143
112
  }
144
113
  }
145
114
  }
@@ -222,7 +191,6 @@ export class JuxCompiler {
222
191
  }
223
192
  });
224
193
 
225
- // Default known facades/components if load fails
226
194
  const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
227
195
 
228
196
  walk(ast, {
@@ -245,15 +213,11 @@ export class JuxCompiler {
245
213
  return issues;
246
214
  }
247
215
 
248
- /**
249
- * Generate entry point - inline EVERYTHING
250
- */
251
216
  generateEntryPoint(views, dataModules, sharedModules) {
252
217
  let entry = `// Auto-generated JUX entry point\n\n`;
253
218
  const sourceSnapshot = {};
254
219
 
255
- // Collect ALL unique imports from ALL files (views + shared + data)
256
- const allImports = new Map(); // Map<resolvedPath, Set<importedNames>>
220
+ const allImports = new Map();
257
221
  const allFiles = [...views, ...sharedModules, ...dataModules];
258
222
 
259
223
  allFiles.forEach(file => {
@@ -267,28 +231,22 @@ export class JuxCompiler {
267
231
  locations: true
268
232
  });
269
233
 
270
- // Extract import statements
271
234
  ast.body.filter(node => node.type === 'ImportDeclaration').forEach(node => {
272
235
  const importPath = node.source.value;
273
236
 
274
- // ✅ Resolve the import path
275
237
  let resolvedImportPath;
276
238
  if (importPath.startsWith('.')) {
277
- // Relative import
278
239
  const absolutePath = path.resolve(path.dirname(filePath), importPath);
279
240
  resolvedImportPath = path.relative(this.srcDir, absolutePath);
280
241
  resolvedImportPath = resolvedImportPath.replace(/\\/g, '/');
281
242
  } else {
282
- // Module import like 'juxscript', 'axios'
283
243
  resolvedImportPath = importPath;
284
244
  }
285
245
 
286
- // Initialize Set for this path
287
246
  if (!allImports.has(resolvedImportPath)) {
288
247
  allImports.set(resolvedImportPath, new Set());
289
248
  }
290
249
 
291
- // Add imported names
292
250
  node.specifiers.forEach(spec => {
293
251
  if (spec.type === 'ImportSpecifier') {
294
252
  allImports.get(resolvedImportPath).add(spec.imported.name);
@@ -302,16 +260,13 @@ export class JuxCompiler {
302
260
  }
303
261
  });
304
262
 
305
- // ✅ Filter out imports that point to our own files (these will be inlined)
306
263
  const externalImports = new Map();
307
264
  allImports.forEach((names, resolvedPath) => {
308
- // Only include if it's NOT a file in our src directory
309
265
  if (!resolvedPath.endsWith('.js') && !resolvedPath.endsWith('.jux')) {
310
266
  externalImports.set(resolvedPath, names);
311
267
  }
312
268
  });
313
269
 
314
- // ✅ Write external imports only (juxscript, axios, etc.)
315
270
  externalImports.forEach((names, importPath) => {
316
271
  const namedImports = Array.from(names).filter(n => !n.startsWith('default:'));
317
272
  const defaultImports = Array.from(names).filter(n => n.startsWith('default:')).map(n => n.split(':')[1]);
@@ -329,7 +284,6 @@ export class JuxCompiler {
329
284
 
330
285
  entry += '\n';
331
286
 
332
- // ✅ Inline shared modules as constants/functions
333
287
  entry += `// --- SHARED MODULES (INLINED) ---\n`;
334
288
  sharedModules.forEach(m => {
335
289
  sourceSnapshot[m.file] = {
@@ -339,14 +293,12 @@ export class JuxCompiler {
339
293
  lines: m.content.split('\n')
340
294
  };
341
295
 
342
- // Remove imports and exports, just keep the code
343
296
  let code = m.originalContent || m.content;
344
297
  code = this._stripImportsAndExports(code);
345
298
 
346
299
  entry += `\n// From: ${m.file}\n${code}\n`;
347
300
  });
348
301
 
349
- // ✅ Inline data modules
350
302
  entry += `\n// --- DATA MODULES (INLINED) ---\n`;
351
303
  dataModules.forEach(m => {
352
304
  sourceSnapshot[m.file] = {
@@ -362,39 +314,26 @@ export class JuxCompiler {
362
314
  entry += `\n// From: ${m.file}\n${code}\n`;
363
315
  });
364
316
 
365
- // ✅ Expose everything to window (since we removed exports)
366
- //entry += `\n// Expose to window\n`;
367
- //entry += `Object.assign(window, { jux, state, registry, stateHistory, Link, link, navLink, externalLink, layer, overlay, VStack, HStack, ZStack, vstack, hstack, zstack });\n`;
368
-
369
317
  entry += `\n// --- VIEW FUNCTIONS ---\n`;
370
318
 
371
319
  const routeToFunctionMap = new Map();
372
320
 
373
- // ✅ Process views: strip imports, inline as functions
374
321
  views.forEach((v, index) => {
375
322
  const functionName = `renderJux${index}`;
376
323
 
377
324
  let codeBody = v.originalContent || v.content;
378
325
  const originalLines = codeBody.split('\n');
379
326
  codeBody = this._stripImportsAndExports(codeBody);
380
- const strippedLines = codeBody.split('\n');
381
-
382
- // Build a line map: for each stripped line, find its original line number
383
- const lineMap = this._buildLineMap(originalLines, strippedLines);
384
327
 
385
328
  sourceSnapshot[v.file] = {
386
329
  name: v.name,
387
330
  file: v.file,
388
- content: v.originalContent || v.content,
389
331
  lines: originalLines,
390
- strippedLines: strippedLines,
391
- lineMap: lineMap,
392
332
  functionName
393
333
  };
394
334
 
395
335
  entry += `\nasync function ${functionName}() {\n${codeBody}\n}\n`;
396
336
 
397
- // Generate route path
398
337
  const routePath = v.name
399
338
  .toLowerCase()
400
339
  .replace(/\\/g, '/')
@@ -411,18 +350,17 @@ export class JuxCompiler {
411
350
  routeToFunctionMap.set(`/${routePath}`, functionName);
412
351
  });
413
352
 
414
- // ✅ Generate router
415
353
  entry += this._generateRouter(routeToFunctionMap);
416
354
 
417
355
  this._sourceSnapshot = sourceSnapshot;
418
356
  this._validationIssues = [];
419
357
 
358
+ // Embed source snapshot for runtime error overlay
359
+ entry += `\nwindow.__juxSources = ${JSON.stringify(sourceSnapshot)};\n`;
360
+
420
361
  return entry;
421
362
  }
422
363
 
423
- /**
424
- * Strip imports and exports from code, keeping the actual logic
425
- */
426
364
  _stripImportsAndExports(code) {
427
365
  try {
428
366
  const ast = acorn.parse(code, {
@@ -431,7 +369,6 @@ export class JuxCompiler {
431
369
  locations: true
432
370
  });
433
371
 
434
- // Remove imports and exports
435
372
  const nodesToRemove = ast.body.filter(node =>
436
373
  node.type === 'ImportDeclaration' ||
437
374
  node.type === 'ExportNamedDeclaration' ||
@@ -443,7 +380,6 @@ export class JuxCompiler {
443
380
  return code.trim();
444
381
  }
445
382
 
446
- // Remove nodes in reverse order
447
383
  let result = code;
448
384
  let offset = 0;
449
385
 
@@ -451,21 +387,17 @@ export class JuxCompiler {
451
387
  let start = node.start - offset;
452
388
  let end = node.end - offset;
453
389
 
454
- // If it's an export with a declaration, keep the declaration
455
390
  if (node.type === 'ExportNamedDeclaration' && node.declaration) {
456
- // Keep the declaration part, remove just "export"
457
391
  const declarationStart = node.declaration.start - offset;
458
392
  result = result.substring(0, start) + result.substring(declarationStart);
459
393
  offset += (declarationStart - start);
460
394
  } else if (node.type === 'ExportDefaultDeclaration') {
461
- // Remove "export default", keep the expression
462
395
  const exportKeyword = result.substring(start, end).match(/export\s+default\s+/);
463
396
  if (exportKeyword) {
464
397
  result = result.substring(0, start) + result.substring(start + exportKeyword[0].length);
465
398
  offset += exportKeyword[0].length;
466
399
  }
467
400
  } else {
468
- // Remove the entire node
469
401
  result = result.substring(0, start) + result.substring(end);
470
402
  offset += (end - start);
471
403
  }
@@ -495,17 +427,11 @@ export class JuxCompiler {
495
427
  }
496
428
  fs.mkdirSync(this.distDir, { recursive: true });
497
429
 
498
- // ✅ Copy public folder if exists
499
430
  this.copyPublicFolder();
500
431
 
501
432
  const { views, dataModules, sharedModules } = this.scanFiles();
502
433
  console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
503
434
 
504
- // ❌ REMOVE: No need to copy files to dist - everything goes in bundle
505
- // const juxDistDir = path.join(this.distDir, 'jux');
506
- // fs.mkdirSync(juxDistDir, { recursive: true });
507
- // ... copying logic removed ...
508
-
509
435
  const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
510
436
 
511
437
  const entryPath = path.join(this.distDir, 'entry.js');
@@ -515,12 +441,6 @@ export class JuxCompiler {
515
441
  fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
516
442
  console.log(`📸 Source snapshot written`);
517
443
 
518
- // ✅ Also write to dist root so the error overlay can fetch it via /__jux_sources.json
519
- const distSnapshotPath = path.join(this.config.distDir, '__jux_sources.json');
520
- if (distSnapshotPath !== snapshotPath) {
521
- fs.writeFileSync(distSnapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
522
- }
523
-
524
444
  const validation = this.reportValidationIssues();
525
445
  if (!validation.isValid) {
526
446
  console.log('🛑 BUILD FAILED\n');
@@ -547,12 +467,10 @@ export class JuxCompiler {
547
467
  plugins: [{
548
468
  name: 'juxscript-resolver',
549
469
  setup: (build) => {
550
- // Resolve juxscript
551
470
  build.onResolve({ filter: /^juxscript$/ }, () => ({
552
471
  path: juxscriptPath
553
472
  }));
554
473
 
555
- // ✅ Force axios to resolve from project's node_modules
556
474
  build.onResolve({ filter: /^axios$/ }, () => {
557
475
  const projectRoot = process.cwd();
558
476
  const axiosPath = path.resolve(projectRoot, 'node_modules/axios/dist/esm/axios.js');
@@ -566,30 +484,22 @@ export class JuxCompiler {
566
484
  return null;
567
485
  });
568
486
 
569
- // ✅ Resolve .jux file imports - with detailed logging
570
487
  build.onResolve({ filter: /\.jux$/ }, (args) => {
571
488
  console.log(`🔍 Resolving: ${args.path} from ${args.importer}`);
572
489
 
573
- // Skip if already resolved
574
490
  if (path.isAbsolute(args.path)) {
575
491
  return { path: args.path };
576
492
  }
577
493
 
578
- // Handle relative imports
579
494
  if (args.path.startsWith('.')) {
580
495
  const importer = args.importer || entryPath;
581
496
  const importerDir = path.dirname(importer);
582
497
  const resolvedPath = path.resolve(importerDir, args.path);
583
498
 
584
- console.log(` Importer dir: ${importerDir}`);
585
- console.log(` Resolved to: ${resolvedPath}`);
586
- console.log(` Exists: ${fs.existsSync(resolvedPath)}`);
587
-
588
499
  if (fs.existsSync(resolvedPath)) {
589
500
  return { path: resolvedPath };
590
501
  } else {
591
502
  console.error(`❌ Could not resolve ${args.path} from ${importer}`);
592
- console.error(` Tried: ${resolvedPath}`);
593
503
  }
594
504
  }
595
505
 
@@ -632,8 +542,7 @@ export class JuxCompiler {
632
542
  <title>JUX Application</title>
633
543
  ${generateErrorCollector({
634
544
  enabled: process.env.NODE_ENV !== 'production',
635
- maxErrors: 50,
636
- position: 'bottom-right'
545
+ maxErrors: 50
637
546
  })}
638
547
  <script type="module" src="/bundle.js"></script>
639
548
  </head>
@@ -652,9 +561,6 @@ export class JuxCompiler {
652
561
  return { success: true, errors: [], warnings: validation.warnings };
653
562
  }
654
563
 
655
- /**
656
- * Generate router code
657
- */
658
564
  _generateRouter(routeToFunctionMap) {
659
565
  let router = `\n// --- ROUTER ---\n`;
660
566
  router += `const routes = {\n`;
@@ -668,14 +574,12 @@ export class JuxCompiler {
668
574
  router += `function route(path) {
669
575
  const renderFn = routes[path] || routes['/'];
670
576
  if (renderFn) {
671
- // Clear main content area
672
577
  const appMain = document.getElementById('appmain-content');
673
578
  if (appMain) appMain.innerHTML = '';
674
579
 
675
580
  const app = document.getElementById('app');
676
581
  if (app) app.innerHTML = '';
677
582
 
678
- // Call render function
679
583
  if (typeof renderFn === 'function') {
680
584
  renderFn();
681
585
  }
@@ -685,15 +589,12 @@ export class JuxCompiler {
685
589
  }
686
590
  }
687
591
 
688
- // Initial route
689
592
  window.addEventListener('DOMContentLoaded', () => {
690
593
  route(window.location.pathname);
691
594
  });
692
595
 
693
- // Handle navigation
694
596
  window.addEventListener('popstate', () => route(window.location.pathname));
695
597
 
696
- // Intercept link clicks for SPA navigation
697
598
  document.addEventListener('click', (e) => {
698
599
  const link = e.target.closest('a[href]');
699
600
  if (link) {
@@ -706,7 +607,6 @@ document.addEventListener('click', (e) => {
706
607
  }
707
608
  });
708
609
 
709
- // Expose router for manual navigation
710
610
  window.navigateTo = (path) => {
711
611
  window.history.pushState({}, '', path);
712
612
  route(path);
@@ -716,14 +616,10 @@ window.navigateTo = (path) => {
716
616
  return router;
717
617
  }
718
618
 
719
- /**
720
- * Report validation issues
721
- */
722
619
  reportValidationIssues() {
723
620
  const errors = [];
724
621
  const warnings = [];
725
622
 
726
- // Check if there are any validation issues collected
727
623
  if (this._validationIssues && this._validationIssues.length > 0) {
728
624
  this._validationIssues.forEach(issue => {
729
625
  if (issue.type === 'error') {
@@ -734,7 +630,6 @@ window.navigateTo = (path) => {
734
630
  });
735
631
  }
736
632
 
737
- // Log warnings
738
633
  if (warnings.length > 0) {
739
634
  console.log('\n⚠️ Warnings:\n');
740
635
  warnings.forEach(w => {
@@ -742,7 +637,6 @@ window.navigateTo = (path) => {
742
637
  });
743
638
  }
744
639
 
745
- // Log errors
746
640
  if (errors.length > 0) {
747
641
  console.log('\n❌ Errors:\n');
748
642
  errors.forEach(e => {
@@ -750,24 +644,16 @@ window.navigateTo = (path) => {
750
644
  });
751
645
  }
752
646
 
753
- return {
754
- isValid: errors.length === 0,
755
- errors,
756
- warnings
757
- };
647
+ return { isValid: errors.length === 0, errors, warnings };
758
648
  }
759
649
 
760
- /**
761
- * Copy public folder contents to dist
762
- */
763
650
  copyPublicFolder() {
764
- // ✅ Use configured public path or resolve from paths object
765
651
  const publicSrc = this.paths.public
766
652
  ? this.paths.public
767
653
  : path.resolve(process.cwd(), this.publicDir);
768
654
 
769
655
  if (!fs.existsSync(publicSrc)) {
770
- return; // No public folder, skip
656
+ return;
771
657
  }
772
658
 
773
659
  console.log('📦 Copying public assets...');
@@ -780,30 +666,23 @@ window.navigateTo = (path) => {
780
666
  }
781
667
  }
782
668
 
783
- /**
784
- * Recursively copy directory contents
785
- */
786
669
  _copyDirRecursive(src, dest, depth = 0) {
787
670
  const entries = fs.readdirSync(src, { withFileTypes: true });
788
671
 
789
672
  entries.forEach(entry => {
790
- // Skip hidden files and directories
791
673
  if (entry.name.startsWith('.')) return;
792
674
 
793
675
  const srcPath = path.join(src, entry.name);
794
676
  const destPath = path.join(dest, entry.name);
795
677
 
796
678
  if (entry.isDirectory()) {
797
- // Create directory and recurse
798
679
  if (!fs.existsSync(destPath)) {
799
680
  fs.mkdirSync(destPath, { recursive: true });
800
681
  }
801
682
  this._copyDirRecursive(srcPath, destPath, depth + 1);
802
683
  } else {
803
- // Copy file
804
684
  fs.copyFileSync(srcPath, destPath);
805
685
 
806
- // Log files at root level only
807
686
  if (depth === 0) {
808
687
  const ext = path.extname(entry.name);
809
688
  const icon = this._getFileIcon(ext);
@@ -813,76 +692,22 @@ window.navigateTo = (path) => {
813
692
  });
814
693
  }
815
694
 
816
- /**
817
- * Get icon for file type
818
- */
819
695
  _getFileIcon(ext) {
820
696
  const icons = {
821
- '.html': '📄',
822
- '.css': '🎨',
823
- '.js': '📜',
824
- '.json': '📋',
825
- '.png': '🖼️',
826
- '.jpg': '🖼️',
827
- '.jpeg': '🖼️',
828
- '.gif': '🖼️',
829
- '.svg': '🎨',
830
- '.ico': '🔖',
831
- '.woff': '🔤',
832
- '.woff2': '🔤',
833
- '.ttf': '🔤',
834
- '.eot': '🔤'
697
+ '.html': '📄', '.css': '🎨', '.js': '📜', '.json': '📋',
698
+ '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️',
699
+ '.svg': '🎨', '.ico': '🔖',
700
+ '.woff': '🔤', '.woff2': '🔤', '.ttf': '🔤', '.eot': '🔤'
835
701
  };
836
702
  return icons[ext.toLowerCase()] || '📦';
837
703
  }
838
704
 
839
- /**
840
- * ✅ Generate valid JavaScript identifier from file path
841
- * Example: abc/aaa.jux -> abc_aaa
842
- * Example: menus/main.jux -> menus_main
843
- * Example: pages/blog/post.jux -> pages_blog_post
844
- */
845
705
  _generateNameFromPath(filepath) {
846
706
  return filepath
847
- .replace(/\.jux$/, '') // Remove .jux extension
848
- .replace(/[\/\\]/g, '_') // Replace / and \ with _
849
- .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
850
- .replace(/_+/g, '_') // Collapse multiple consecutive underscores
851
- .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
707
+ .replace(/\.jux$/, '')
708
+ .replace(/[\/\\]/g, '_')
709
+ .replace(/[^a-zA-Z0-9_]/g, '_')
710
+ .replace(/_+/g, '_')
711
+ .replace(/^_|_$/g, '');
852
712
  }
853
-
854
- /**
855
- * Build a mapping from stripped line index to original source line number.
856
- * For each line in strippedLines, find the best matching line in originalLines.
857
- */
858
- _buildLineMap(originalLines, strippedLines) {
859
- const lineMap = [];
860
- let searchFrom = 0;
861
-
862
- for (let i = 0; i < strippedLines.length; i++) {
863
- const stripped = strippedLines[i].trim();
864
- if (!stripped) {
865
- // Empty line — estimate position
866
- lineMap.push(searchFrom < originalLines.length ? searchFrom + 1 : originalLines.length);
867
- continue;
868
- }
869
-
870
- let found = false;
871
- for (let j = searchFrom; j < originalLines.length; j++) {
872
- if (originalLines[j].trim() === stripped) {
873
- lineMap.push(j + 1); // 1-based
874
- searchFrom = j + 1;
875
- found = true;
876
- break;
877
- }
878
- }
879
-
880
- if (!found) {
881
- // Fallback: use last known position
882
- lineMap.push(searchFrom < originalLines.length ? searchFrom + 1 : originalLines.length);
883
- }
884
- }
885
-
886
- return lineMap;
887
- }
888
- }
713
+ }
@@ -1,383 +1,109 @@
1
1
  /**
2
- * Generates an injectable client-side error collector script.
3
- * Full-screen overlay similar to Vite's error screen.
4
- * Resolves bundle line numbers back to original .jux source using __jux_sources.json.
5
- *
6
- * @param {Object} options
7
- * @param {boolean} [options.enabled=true]
8
- * @param {number} [options.maxErrors=50]
9
- * @returns {string} Inline script to inject into HTML
2
+ * Injectable client-side error overlay.
3
+ * Matches renderJuxN in stack traces to show original .jux source.
10
4
  */
11
5
  export function generateErrorCollector(options = {}) {
12
- const {
13
- enabled = true,
14
- maxErrors = 50
15
- } = options;
16
-
6
+ const { enabled = true, maxErrors = 50 } = options;
17
7
  if (!enabled) return '';
18
8
 
19
9
  return `
20
10
  <script>
21
11
  (function() {
22
12
  var __errors = [];
23
- var __maxErrors = ${maxErrors};
24
13
  var __overlay = null;
25
14
  var __list = null;
26
- var __sources = null;
27
- var __sourcesFetched = false;
28
- var __bundleLines = null;
29
-
30
- // Fetch source map + bundle on first error
31
- function loadSources(cb) {
32
- if (__sourcesFetched) return cb();
33
- __sourcesFetched = true;
34
-
35
- var done = 0;
36
- function check() { if (++done >= 2) cb(); }
37
-
38
- // Fetch source snapshot
39
- var xhr1 = new XMLHttpRequest();
40
- xhr1.open('GET', '/__jux_sources.json', true);
41
- xhr1.onload = function() {
42
- try { __sources = JSON.parse(xhr1.responseText); } catch(e) {}
43
- check();
44
- };
45
- xhr1.onerror = check;
46
- xhr1.send();
47
15
 
48
- // Fetch bundle to split into lines
49
- var xhr2 = new XMLHttpRequest();
50
- xhr2.open('GET', '/bundle.js', true);
51
- xhr2.onload = function() {
52
- try { __bundleLines = xhr2.responseText.split('\\n'); } catch(e) {}
53
- check();
54
- };
55
- xhr2.onerror = check;
56
- xhr2.send();
57
- }
58
-
59
- // Given a bundle line number, find which view function it belongs to
60
- // and map back to the original .jux source line
61
- function resolveSourceLocation(bundleLine) {
62
- if (!__sources || !__bundleLines || !bundleLine) return null;
63
-
64
- var funcName = null;
65
- var funcStartLine = 0;
66
- for (var i = bundleLine - 1; i >= 0; i--) {
67
- var line = __bundleLines[i];
68
- if (!line) continue;
69
- var match = line.match(/async\\s+function\\s+(renderJux\\d+)/);
70
- if (match) {
71
- funcName = match[1];
72
- funcStartLine = i + 1;
73
- break;
74
- }
16
+ function findSource(stack) {
17
+ var sources = window.__juxSources;
18
+ if (!sources || !stack) return null;
19
+ var m = stack.match(/renderJux\\d+/);
20
+ if (!m) return null;
21
+ for (var key in sources) {
22
+ if (sources[key].functionName === m[0]) return sources[key];
75
23
  }
24
+ return null;
25
+ }
76
26
 
77
- if (!funcName) return null;
78
-
79
- var sourceEntry = null;
80
- for (var key in __sources) {
81
- if (__sources[key].functionName === funcName) {
82
- sourceEntry = __sources[key];
83
- break;
84
- }
85
- }
86
-
87
- if (!sourceEntry) return null;
88
-
89
- // Index into the function body (0-based)
90
- var offsetInFunc = bundleLine - funcStartLine - 1;
91
- var lineMap = sourceEntry.lineMap || [];
92
- var originalLines = sourceEntry.lines || [];
93
-
94
- // Use lineMap for accurate resolution
95
- var originalLineNum;
96
- if (lineMap.length > 0 && offsetInFunc >= 0 && offsetInFunc < lineMap.length) {
97
- originalLineNum = lineMap[offsetInFunc]; // already 1-based
98
- } else {
99
- // Fallback: direct offset
100
- originalLineNum = Math.max(1, Math.min(offsetInFunc + 1, originalLines.length));
101
- }
102
-
103
- var lineIndex = originalLineNum - 1;
104
- var contextRadius = 3;
105
- var contextStart = Math.max(0, lineIndex - contextRadius);
106
- var contextEnd = Math.min(originalLines.length, lineIndex + contextRadius + 1);
107
-
108
- return {
109
- file: sourceEntry.file,
110
- name: sourceEntry.name,
111
- functionName: funcName,
112
- originalLine: originalLineNum,
113
- originalCode: originalLines[lineIndex] || '',
114
- context: originalLines.slice(contextStart, contextEnd),
115
- contextStartLine: contextStart + 1,
116
- highlightLine: originalLineNum
117
- };
27
+ function escapeHtml(s) {
28
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
118
29
  }
119
30
 
120
- // Parse stack trace and extract bundle line numbers
121
- function parseStack(stack) {
122
- if (!stack) return [];
123
- var frames = [];
124
- var lines = stack.split('\\n');
31
+ function renderSource(src) {
32
+ if (!src) return '';
33
+ var lines = src.lines || [];
34
+ var html = '<div style="margin-top:8px;padding:8px;background:#111;border:1px solid #333;border-radius:4px;">';
35
+ html += '<div style="color:#ff5555;font-size:12px;margin-bottom:6px;font-weight:bold;">' + escapeHtml(src.file) + '</div>';
125
36
  for (var i = 0; i < lines.length; i++) {
126
- // Match patterns like "at funcName (url:line:col)" or "at url:line:col"
127
- var m = lines[i].match(/(?:at\\s+(.+?)\\s+\\()?(?:https?:\\/\\/[^:]+):([\\d]+):([\\d]+)\\)?/);
128
- if (m) {
129
- frames.push({
130
- raw: lines[i].trim(),
131
- func: m[1] || '(anonymous)',
132
- line: parseInt(m[2], 10),
133
- col: parseInt(m[3], 10)
134
- });
135
- }
37
+ html += '<div style="padding:1px 8px;white-space:pre;">' +
38
+ '<span style="color:#555;display:inline-block;width:32px;text-align:right;margin-right:12px;user-select:none;">' + (i+1) + '</span>' +
39
+ '<span style="color:#ccc;">' + escapeHtml(lines[i]) + '</span></div>';
136
40
  }
137
- return frames;
41
+ html += '</div>';
42
+ return html;
138
43
  }
139
44
 
140
45
  function createOverlay() {
141
46
  __overlay = document.createElement('div');
142
- __overlay.id = '__jux-error-overlay';
143
- __overlay.setAttribute('style', [
144
- 'position:fixed',
145
- 'top:0',
146
- 'left:0',
147
- 'width:100vw',
148
- 'height:100vh',
149
- 'z-index:99999',
150
- 'background:rgba(0,0,0,0.85)',
151
- 'color:#f8f8f8',
152
- 'font-family:monospace',
153
- 'font-size:14px',
154
- 'overflow-y:auto',
155
- 'padding:0',
156
- 'margin:0',
157
- 'box-sizing:border-box'
158
- ].join(';'));
159
-
160
- var container = document.createElement('div');
161
- container.setAttribute('style', [
162
- 'max-width:960px',
163
- 'margin:40px auto',
164
- 'padding:24px 32px',
165
- 'border:2px solid #ff5555',
166
- 'border-radius:8px',
167
- 'background:#1a1a1a'
168
- ].join(';'));
169
-
170
- var header = document.createElement('div');
171
- header.setAttribute('style', [
172
- 'display:flex',
173
- 'justify-content:space-between',
174
- 'align-items:center',
175
- 'margin-bottom:16px',
176
- 'padding-bottom:12px',
177
- 'border-bottom:1px solid #333'
178
- ].join(';'));
179
-
180
- var title = document.createElement('div');
181
- title.setAttribute('style', 'color:#ff5555;font-size:16px;font-weight:bold;');
182
- title.textContent = 'Runtime Error';
183
- header.appendChild(title);
184
-
185
- var actions = document.createElement('div');
186
- actions.setAttribute('style', 'display:flex;gap:8px;');
187
-
188
- var clearBtn = document.createElement('button');
189
- clearBtn.textContent = 'Clear';
190
- clearBtn.setAttribute('style', [
191
- 'background:transparent',
192
- 'border:1px solid #555',
193
- 'color:#aaa',
194
- 'padding:4px 12px',
195
- 'border-radius:4px',
196
- 'cursor:pointer',
197
- 'font-family:monospace',
198
- 'font-size:12px'
199
- ].join(';'));
200
- clearBtn.addEventListener('click', clearErrors);
201
-
202
- var closeBtn = document.createElement('button');
203
- closeBtn.textContent = 'Close';
204
- closeBtn.setAttribute('style', [
205
- 'background:transparent',
206
- 'border:1px solid #555',
207
- 'color:#aaa',
208
- 'padding:4px 12px',
209
- 'border-radius:4px',
210
- 'cursor:pointer',
211
- 'font-family:monospace',
212
- 'font-size:12px'
213
- ].join(';'));
214
- closeBtn.addEventListener('click', function() {
215
- __overlay.style.display = 'none';
216
- });
217
-
218
- actions.appendChild(clearBtn);
219
- actions.appendChild(closeBtn);
220
- header.appendChild(actions);
221
- container.appendChild(header);
222
-
47
+ __overlay.setAttribute('style','position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;background:rgba(0,0,0,0.85);color:#f8f8f8;font-family:monospace;font-size:14px;overflow-y:auto;');
48
+ var c = document.createElement('div');
49
+ c.setAttribute('style','max-width:960px;margin:40px auto;padding:24px 32px;border:2px solid #ff5555;border-radius:8px;background:#1a1a1a;');
50
+ var h = document.createElement('div');
51
+ h.setAttribute('style','display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid #333;');
52
+ var t = document.createElement('div');
53
+ t.setAttribute('style','color:#ff5555;font-size:16px;font-weight:bold;');
54
+ t.textContent = 'Runtime Error';
55
+ var btn = document.createElement('button');
56
+ btn.textContent = 'Close';
57
+ btn.setAttribute('style','background:transparent;border:1px solid #555;color:#aaa;padding:4px 12px;border-radius:4px;cursor:pointer;font-family:monospace;font-size:12px;');
58
+ btn.addEventListener('click', function() { __overlay.style.display = 'none'; });
59
+ h.appendChild(t); h.appendChild(btn); c.appendChild(h);
223
60
  __list = document.createElement('div');
224
- container.appendChild(__list);
225
-
226
- __overlay.appendChild(container);
61
+ c.appendChild(__list); __overlay.appendChild(c);
227
62
  document.body.appendChild(__overlay);
228
63
  }
229
64
 
230
- function clearErrors() {
231
- __errors = [];
232
- if (__list) __list.innerHTML = '';
233
- if (__overlay) __overlay.style.display = 'none';
234
- }
235
-
236
- function renderSourceContext(resolved) {
237
- if (!resolved) return '';
238
-
239
- var lines = resolved.context || [];
240
- var startLine = resolved.contextStartLine || 1;
241
- var highlight = resolved.highlightLine || -1;
242
-
243
- var html = '<div style="margin-top:8px;padding:8px;background:#111;border:1px solid #333;border-radius:4px;">';
244
- html += '<div style="color:#ff5555;font-size:12px;margin-bottom:6px;font-weight:bold;">' +
245
- escapeHtml(resolved.file) + ':' + resolved.originalLine +
246
- '</div>';
247
-
248
- for (var i = 0; i < lines.length; i++) {
249
- var lineNum = startLine + i;
250
- var isHighlight = lineNum === highlight;
251
- var bg = isHighlight ? 'background:#3a1a1a;' : '';
252
- var color = isHighlight ? 'color:#ff8888;' : 'color:#888;';
253
- var numColor = isHighlight ? 'color:#ff5555;' : 'color:#555;';
254
- var marker = isHighlight ? '>' : ' ';
255
-
256
- html += '<div style="' + bg + 'padding:1px 8px;white-space:pre;">' +
257
- '<span style="' + numColor + 'display:inline-block;width:40px;text-align:right;margin-right:12px;user-select:none;">' +
258
- marker + ' ' + lineNum +
259
- '</span>' +
260
- '<span style="' + color + '">' + escapeHtml(lines[i]) + '</span>' +
261
- '</div>';
262
- }
263
-
264
- html += '</div>';
265
- return html;
266
- }
267
-
268
- function addError(type, message, source, line, col, stack) {
269
- if (__errors.length >= __maxErrors) __errors.shift();
270
-
271
- var entry = {
272
- type: type,
273
- message: String(message || ''),
274
- source: source || '',
275
- line: line || 0,
276
- col: col || 0,
277
- stack: stack || '',
278
- time: new Date().toLocaleTimeString()
279
- };
280
- __errors.push(entry);
281
-
65
+ function addError(type, message, stack) {
66
+ if (__errors.length >= ${maxErrors}) __errors.shift();
67
+ __errors.push({ type: type, message: message, stack: stack });
282
68
  if (!__overlay) createOverlay();
283
69
  __overlay.style.display = 'block';
284
70
 
285
71
  var item = document.createElement('div');
286
- item.setAttribute('style', [
287
- 'padding:12px 16px',
288
- 'margin-bottom:8px',
289
- 'background:#222',
290
- 'border-left:3px solid #ff5555',
291
- 'border-radius:4px'
292
- ].join(';'));
72
+ item.setAttribute('style','padding:12px 16px;margin-bottom:8px;background:#222;border-left:3px solid #ff5555;border-radius:4px;');
293
73
 
294
- var typeLabel = type === 'error' ? 'Error' : type === 'rejection' ? 'Unhandled Rejection' : 'console.error';
74
+ var label = type === 'error' ? 'Error' : type === 'rejection' ? 'Unhandled Rejection' : 'console.error';
75
+ item.innerHTML = '<div style="color:#888;font-size:12px;margin-bottom:4px;">' + label + '</div>' +
76
+ '<div style="color:#ff8888;font-size:14px;white-space:pre-wrap;word-break:break-word;font-weight:bold;">' + escapeHtml(message) + '</div>';
295
77
 
296
- item.innerHTML =
297
- '<div style="color:#888;font-size:12px;margin-bottom:4px;">' +
298
- typeLabel +
299
- '<span style="float:right;">' + entry.time + '</span>' +
300
- '</div>' +
301
- '<div style="color:#ff8888;font-size:14px;white-space:pre-wrap;word-break:break-word;font-weight:bold;">' +
302
- escapeHtml(entry.message) +
303
- '</div>';
78
+ var src = findSource(stack);
79
+ if (src) item.innerHTML += renderSource(src);
304
80
 
81
+ if (stack) {
82
+ var d = document.createElement('details');
83
+ d.setAttribute('style','margin-top:8px;');
84
+ d.innerHTML = '<summary style="cursor:pointer;color:#555;font-size:12px;">Stack trace</summary>' +
85
+ '<pre style="margin:4px 0 0;padding:8px;font-size:12px;color:#666;white-space:pre-wrap;background:#111;border:1px solid #333;border-radius:4px;max-height:200px;overflow-y:auto;">' + escapeHtml(stack) + '</pre>';
86
+ item.appendChild(d);
87
+ }
305
88
  __list.appendChild(item);
306
-
307
- // Resolve source location asynchronously
308
- loadSources(function() {
309
- var frames = parseStack(entry.stack);
310
- var resolved = null;
311
-
312
- // Try each frame until we find one that maps to a .jux source
313
- for (var i = 0; i < frames.length; i++) {
314
- var r = resolveSourceLocation(frames[i].line);
315
- if (r) {
316
- resolved = r;
317
- break;
318
- }
319
- }
320
-
321
- // Also try the direct line/col from the error itself
322
- if (!resolved && entry.line) {
323
- resolved = resolveSourceLocation(entry.line);
324
- }
325
-
326
- if (resolved) {
327
- // Add source context block
328
- var sourceBlock = document.createElement('div');
329
- sourceBlock.innerHTML = renderSourceContext(resolved);
330
- item.appendChild(sourceBlock);
331
- }
332
-
333
- // Add raw stack (collapsed) if present
334
- if (entry.stack) {
335
- var stackBlock = document.createElement('details');
336
- stackBlock.setAttribute('style', 'margin-top:8px;');
337
- stackBlock.innerHTML =
338
- '<summary style="cursor:pointer;color:#555;font-size:12px;user-select:none;">Raw stack trace</summary>' +
339
- '<pre style="margin:4px 0 0;padding:8px;font-size:12px;color:#666;' +
340
- 'white-space:pre-wrap;background:#111;border-radius:4px;' +
341
- 'border:1px solid #333;max-height:200px;overflow-y:auto;">' +
342
- escapeHtml(entry.stack) +
343
- '</pre>';
344
- item.appendChild(stackBlock);
345
- }
346
- });
347
89
  }
348
90
 
349
- function escapeHtml(str) {
350
- return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
351
- }
352
-
353
- // --- Hooks ---
354
-
355
- window.onerror = function(msg, source, line, col, err) {
356
- addError('error', msg, source, line, col, err && err.stack ? err.stack : '');
91
+ window.onerror = function(msg, src, line, col, err) {
92
+ addError('error', msg, err && err.stack ? err.stack : '');
357
93
  };
358
-
359
94
  window.addEventListener('unhandledrejection', function(e) {
360
- var reason = e.reason;
361
- var msg = reason instanceof Error ? reason.message : String(reason);
362
- var stack = reason instanceof Error ? reason.stack : '';
363
- addError('rejection', msg, '', 0, 0, stack);
95
+ var r = e.reason;
96
+ addError('rejection', r instanceof Error ? r.message : String(r), r instanceof Error ? r.stack : '');
364
97
  });
365
-
366
- var _origConsoleError = console.error;
98
+ var _ce = console.error;
367
99
  console.error = function() {
368
- var args = Array.prototype.slice.call(arguments);
369
- var msg = args.map(function(a) {
370
- return typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a);
100
+ var msg = Array.prototype.slice.call(arguments).map(function(a) {
101
+ return typeof a === 'object' ? JSON.stringify(a) : String(a);
371
102
  }).join(' ');
372
- addError('console', msg);
373
- _origConsoleError.apply(console, arguments);
374
- };
375
-
376
- window.__juxErrors = {
377
- list: function() { return __errors.slice(); },
378
- clear: clearErrors,
379
- count: function() { return __errors.length; }
103
+ addError('console', msg, '');
104
+ _ce.apply(console, arguments);
380
105
  };
106
+ window.__juxErrors = { list: function() { return __errors.slice(); }, count: function() { return __errors.length; } };
381
107
  })();
382
108
  </script>`;
383
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.157",
3
+ "version": "1.1.158",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",