juxscript 1.1.156 → 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,32 +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
 
324
+ let codeBody = v.originalContent || v.content;
325
+ const originalLines = codeBody.split('\n');
326
+ codeBody = this._stripImportsAndExports(codeBody);
327
+
377
328
  sourceSnapshot[v.file] = {
378
329
  name: v.name,
379
330
  file: v.file,
380
- content: v.originalContent || v.content,
381
- lines: (v.originalContent || v.content).split('\n'),
331
+ lines: originalLines,
382
332
  functionName
383
333
  };
384
334
 
385
- let codeBody = v.originalContent || v.content;
386
- codeBody = this._stripImportsAndExports(codeBody);
387
-
388
335
  entry += `\nasync function ${functionName}() {\n${codeBody}\n}\n`;
389
336
 
390
- // Generate route path
391
337
  const routePath = v.name
392
338
  .toLowerCase()
393
339
  .replace(/\\/g, '/')
@@ -404,18 +350,17 @@ export class JuxCompiler {
404
350
  routeToFunctionMap.set(`/${routePath}`, functionName);
405
351
  });
406
352
 
407
- // ✅ Generate router
408
353
  entry += this._generateRouter(routeToFunctionMap);
409
354
 
410
355
  this._sourceSnapshot = sourceSnapshot;
411
356
  this._validationIssues = [];
412
357
 
358
+ // Embed source snapshot for runtime error overlay
359
+ entry += `\nwindow.__juxSources = ${JSON.stringify(sourceSnapshot)};\n`;
360
+
413
361
  return entry;
414
362
  }
415
363
 
416
- /**
417
- * Strip imports and exports from code, keeping the actual logic
418
- */
419
364
  _stripImportsAndExports(code) {
420
365
  try {
421
366
  const ast = acorn.parse(code, {
@@ -424,7 +369,6 @@ export class JuxCompiler {
424
369
  locations: true
425
370
  });
426
371
 
427
- // Remove imports and exports
428
372
  const nodesToRemove = ast.body.filter(node =>
429
373
  node.type === 'ImportDeclaration' ||
430
374
  node.type === 'ExportNamedDeclaration' ||
@@ -436,7 +380,6 @@ export class JuxCompiler {
436
380
  return code.trim();
437
381
  }
438
382
 
439
- // Remove nodes in reverse order
440
383
  let result = code;
441
384
  let offset = 0;
442
385
 
@@ -444,21 +387,17 @@ export class JuxCompiler {
444
387
  let start = node.start - offset;
445
388
  let end = node.end - offset;
446
389
 
447
- // If it's an export with a declaration, keep the declaration
448
390
  if (node.type === 'ExportNamedDeclaration' && node.declaration) {
449
- // Keep the declaration part, remove just "export"
450
391
  const declarationStart = node.declaration.start - offset;
451
392
  result = result.substring(0, start) + result.substring(declarationStart);
452
393
  offset += (declarationStart - start);
453
394
  } else if (node.type === 'ExportDefaultDeclaration') {
454
- // Remove "export default", keep the expression
455
395
  const exportKeyword = result.substring(start, end).match(/export\s+default\s+/);
456
396
  if (exportKeyword) {
457
397
  result = result.substring(0, start) + result.substring(start + exportKeyword[0].length);
458
398
  offset += exportKeyword[0].length;
459
399
  }
460
400
  } else {
461
- // Remove the entire node
462
401
  result = result.substring(0, start) + result.substring(end);
463
402
  offset += (end - start);
464
403
  }
@@ -488,17 +427,11 @@ export class JuxCompiler {
488
427
  }
489
428
  fs.mkdirSync(this.distDir, { recursive: true });
490
429
 
491
- // ✅ Copy public folder if exists
492
430
  this.copyPublicFolder();
493
431
 
494
432
  const { views, dataModules, sharedModules } = this.scanFiles();
495
433
  console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
496
434
 
497
- // ❌ REMOVE: No need to copy files to dist - everything goes in bundle
498
- // const juxDistDir = path.join(this.distDir, 'jux');
499
- // fs.mkdirSync(juxDistDir, { recursive: true });
500
- // ... copying logic removed ...
501
-
502
435
  const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
503
436
 
504
437
  const entryPath = path.join(this.distDir, 'entry.js');
@@ -508,12 +441,6 @@ export class JuxCompiler {
508
441
  fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
509
442
  console.log(`📸 Source snapshot written`);
510
443
 
511
- // ✅ Also write to dist root so the error overlay can fetch it via /__jux_sources.json
512
- const distSnapshotPath = path.join(this.config.distDir, '__jux_sources.json');
513
- if (distSnapshotPath !== snapshotPath) {
514
- fs.writeFileSync(distSnapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
515
- }
516
-
517
444
  const validation = this.reportValidationIssues();
518
445
  if (!validation.isValid) {
519
446
  console.log('🛑 BUILD FAILED\n');
@@ -540,12 +467,10 @@ export class JuxCompiler {
540
467
  plugins: [{
541
468
  name: 'juxscript-resolver',
542
469
  setup: (build) => {
543
- // Resolve juxscript
544
470
  build.onResolve({ filter: /^juxscript$/ }, () => ({
545
471
  path: juxscriptPath
546
472
  }));
547
473
 
548
- // ✅ Force axios to resolve from project's node_modules
549
474
  build.onResolve({ filter: /^axios$/ }, () => {
550
475
  const projectRoot = process.cwd();
551
476
  const axiosPath = path.resolve(projectRoot, 'node_modules/axios/dist/esm/axios.js');
@@ -559,30 +484,22 @@ export class JuxCompiler {
559
484
  return null;
560
485
  });
561
486
 
562
- // ✅ Resolve .jux file imports - with detailed logging
563
487
  build.onResolve({ filter: /\.jux$/ }, (args) => {
564
488
  console.log(`🔍 Resolving: ${args.path} from ${args.importer}`);
565
489
 
566
- // Skip if already resolved
567
490
  if (path.isAbsolute(args.path)) {
568
491
  return { path: args.path };
569
492
  }
570
493
 
571
- // Handle relative imports
572
494
  if (args.path.startsWith('.')) {
573
495
  const importer = args.importer || entryPath;
574
496
  const importerDir = path.dirname(importer);
575
497
  const resolvedPath = path.resolve(importerDir, args.path);
576
498
 
577
- console.log(` Importer dir: ${importerDir}`);
578
- console.log(` Resolved to: ${resolvedPath}`);
579
- console.log(` Exists: ${fs.existsSync(resolvedPath)}`);
580
-
581
499
  if (fs.existsSync(resolvedPath)) {
582
500
  return { path: resolvedPath };
583
501
  } else {
584
502
  console.error(`❌ Could not resolve ${args.path} from ${importer}`);
585
- console.error(` Tried: ${resolvedPath}`);
586
503
  }
587
504
  }
588
505
 
@@ -625,8 +542,7 @@ export class JuxCompiler {
625
542
  <title>JUX Application</title>
626
543
  ${generateErrorCollector({
627
544
  enabled: process.env.NODE_ENV !== 'production',
628
- maxErrors: 50,
629
- position: 'bottom-right'
545
+ maxErrors: 50
630
546
  })}
631
547
  <script type="module" src="/bundle.js"></script>
632
548
  </head>
@@ -645,9 +561,6 @@ export class JuxCompiler {
645
561
  return { success: true, errors: [], warnings: validation.warnings };
646
562
  }
647
563
 
648
- /**
649
- * Generate router code
650
- */
651
564
  _generateRouter(routeToFunctionMap) {
652
565
  let router = `\n// --- ROUTER ---\n`;
653
566
  router += `const routes = {\n`;
@@ -661,14 +574,12 @@ export class JuxCompiler {
661
574
  router += `function route(path) {
662
575
  const renderFn = routes[path] || routes['/'];
663
576
  if (renderFn) {
664
- // Clear main content area
665
577
  const appMain = document.getElementById('appmain-content');
666
578
  if (appMain) appMain.innerHTML = '';
667
579
 
668
580
  const app = document.getElementById('app');
669
581
  if (app) app.innerHTML = '';
670
582
 
671
- // Call render function
672
583
  if (typeof renderFn === 'function') {
673
584
  renderFn();
674
585
  }
@@ -678,15 +589,12 @@ export class JuxCompiler {
678
589
  }
679
590
  }
680
591
 
681
- // Initial route
682
592
  window.addEventListener('DOMContentLoaded', () => {
683
593
  route(window.location.pathname);
684
594
  });
685
595
 
686
- // Handle navigation
687
596
  window.addEventListener('popstate', () => route(window.location.pathname));
688
597
 
689
- // Intercept link clicks for SPA navigation
690
598
  document.addEventListener('click', (e) => {
691
599
  const link = e.target.closest('a[href]');
692
600
  if (link) {
@@ -699,7 +607,6 @@ document.addEventListener('click', (e) => {
699
607
  }
700
608
  });
701
609
 
702
- // Expose router for manual navigation
703
610
  window.navigateTo = (path) => {
704
611
  window.history.pushState({}, '', path);
705
612
  route(path);
@@ -709,14 +616,10 @@ window.navigateTo = (path) => {
709
616
  return router;
710
617
  }
711
618
 
712
- /**
713
- * Report validation issues
714
- */
715
619
  reportValidationIssues() {
716
620
  const errors = [];
717
621
  const warnings = [];
718
622
 
719
- // Check if there are any validation issues collected
720
623
  if (this._validationIssues && this._validationIssues.length > 0) {
721
624
  this._validationIssues.forEach(issue => {
722
625
  if (issue.type === 'error') {
@@ -727,7 +630,6 @@ window.navigateTo = (path) => {
727
630
  });
728
631
  }
729
632
 
730
- // Log warnings
731
633
  if (warnings.length > 0) {
732
634
  console.log('\n⚠️ Warnings:\n');
733
635
  warnings.forEach(w => {
@@ -735,7 +637,6 @@ window.navigateTo = (path) => {
735
637
  });
736
638
  }
737
639
 
738
- // Log errors
739
640
  if (errors.length > 0) {
740
641
  console.log('\n❌ Errors:\n');
741
642
  errors.forEach(e => {
@@ -743,24 +644,16 @@ window.navigateTo = (path) => {
743
644
  });
744
645
  }
745
646
 
746
- return {
747
- isValid: errors.length === 0,
748
- errors,
749
- warnings
750
- };
647
+ return { isValid: errors.length === 0, errors, warnings };
751
648
  }
752
649
 
753
- /**
754
- * Copy public folder contents to dist
755
- */
756
650
  copyPublicFolder() {
757
- // ✅ Use configured public path or resolve from paths object
758
651
  const publicSrc = this.paths.public
759
652
  ? this.paths.public
760
653
  : path.resolve(process.cwd(), this.publicDir);
761
654
 
762
655
  if (!fs.existsSync(publicSrc)) {
763
- return; // No public folder, skip
656
+ return;
764
657
  }
765
658
 
766
659
  console.log('📦 Copying public assets...');
@@ -773,30 +666,23 @@ window.navigateTo = (path) => {
773
666
  }
774
667
  }
775
668
 
776
- /**
777
- * Recursively copy directory contents
778
- */
779
669
  _copyDirRecursive(src, dest, depth = 0) {
780
670
  const entries = fs.readdirSync(src, { withFileTypes: true });
781
671
 
782
672
  entries.forEach(entry => {
783
- // Skip hidden files and directories
784
673
  if (entry.name.startsWith('.')) return;
785
674
 
786
675
  const srcPath = path.join(src, entry.name);
787
676
  const destPath = path.join(dest, entry.name);
788
677
 
789
678
  if (entry.isDirectory()) {
790
- // Create directory and recurse
791
679
  if (!fs.existsSync(destPath)) {
792
680
  fs.mkdirSync(destPath, { recursive: true });
793
681
  }
794
682
  this._copyDirRecursive(srcPath, destPath, depth + 1);
795
683
  } else {
796
- // Copy file
797
684
  fs.copyFileSync(srcPath, destPath);
798
685
 
799
- // Log files at root level only
800
686
  if (depth === 0) {
801
687
  const ext = path.extname(entry.name);
802
688
  const icon = this._getFileIcon(ext);
@@ -806,41 +692,22 @@ window.navigateTo = (path) => {
806
692
  });
807
693
  }
808
694
 
809
- /**
810
- * Get icon for file type
811
- */
812
695
  _getFileIcon(ext) {
813
696
  const icons = {
814
- '.html': '📄',
815
- '.css': '🎨',
816
- '.js': '📜',
817
- '.json': '📋',
818
- '.png': '🖼️',
819
- '.jpg': '🖼️',
820
- '.jpeg': '🖼️',
821
- '.gif': '🖼️',
822
- '.svg': '🎨',
823
- '.ico': '🔖',
824
- '.woff': '🔤',
825
- '.woff2': '🔤',
826
- '.ttf': '🔤',
827
- '.eot': '🔤'
697
+ '.html': '📄', '.css': '🎨', '.js': '📜', '.json': '📋',
698
+ '.png': '🖼️', '.jpg': '🖼️', '.jpeg': '🖼️', '.gif': '🖼️',
699
+ '.svg': '🎨', '.ico': '🔖',
700
+ '.woff': '🔤', '.woff2': '🔤', '.ttf': '🔤', '.eot': '🔤'
828
701
  };
829
702
  return icons[ext.toLowerCase()] || '📦';
830
703
  }
831
704
 
832
- /**
833
- * ✅ Generate valid JavaScript identifier from file path
834
- * Example: abc/aaa.jux -> abc_aaa
835
- * Example: menus/main.jux -> menus_main
836
- * Example: pages/blog/post.jux -> pages_blog_post
837
- */
838
705
  _generateNameFromPath(filepath) {
839
706
  return filepath
840
- .replace(/\.jux$/, '') // Remove .jux extension
841
- .replace(/[\/\\]/g, '_') // Replace / and \ with _
842
- .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
843
- .replace(/_+/g, '_') // Collapse multiple consecutive underscores
844
- .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, '');
845
712
  }
846
- }
713
+ }
@@ -1,378 +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
-
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
15
 
64
- // Find the renderJuxN function that contains this line
65
- // by scanning backwards from bundleLine for "async function renderJux"
66
- var funcName = null;
67
- var funcStartLine = 0;
68
- for (var i = bundleLine - 1; i >= 0; i--) {
69
- var line = __bundleLines[i];
70
- if (!line) continue;
71
- var match = line.match(/async\\s+function\\s+(renderJux\\d+)/);
72
- if (match) {
73
- funcName = match[1];
74
- funcStartLine = i + 1; // 1-based
75
- break;
76
- }
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];
77
23
  }
24
+ return null;
25
+ }
78
26
 
79
- if (!funcName) return null;
80
-
81
- // Find the source entry that maps to this function
82
- var sourceEntry = null;
83
- for (var key in __sources) {
84
- if (__sources[key].functionName === funcName) {
85
- sourceEntry = __sources[key];
86
- break;
87
- }
88
- }
89
-
90
- if (!sourceEntry) return null;
91
-
92
- // Calculate approximate original line
93
- // bundleLine is inside the function body, funcStartLine is the function declaration
94
- // +1 because function body starts after the declaration line
95
- var offsetInFunc = bundleLine - funcStartLine - 1;
96
- var originalLines = sourceEntry.lines || [];
97
- var approxLine = Math.max(0, Math.min(offsetInFunc, originalLines.length - 1));
98
-
99
- return {
100
- file: sourceEntry.file,
101
- name: sourceEntry.name,
102
- functionName: funcName,
103
- originalLine: approxLine + 1,
104
- originalCode: originalLines[approxLine] || '',
105
- // Provide surrounding context (3 lines before/after)
106
- context: originalLines.slice(
107
- Math.max(0, approxLine - 3),
108
- Math.min(originalLines.length, approxLine + 4)
109
- ),
110
- contextStartLine: Math.max(0, approxLine - 3) + 1,
111
- highlightLine: approxLine + 1
112
- };
27
+ function escapeHtml(s) {
28
+ return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
113
29
  }
114
30
 
115
- // Parse stack trace and extract bundle line numbers
116
- function parseStack(stack) {
117
- if (!stack) return [];
118
- var frames = [];
119
- 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>';
120
36
  for (var i = 0; i < lines.length; i++) {
121
- // Match patterns like "at funcName (url:line:col)" or "at url:line:col"
122
- var m = lines[i].match(/(?:at\\s+(.+?)\\s+\\()?(?:https?:\\/\\/[^:]+):([\\d]+):([\\d]+)\\)?/);
123
- if (m) {
124
- frames.push({
125
- raw: lines[i].trim(),
126
- func: m[1] || '(anonymous)',
127
- line: parseInt(m[2], 10),
128
- col: parseInt(m[3], 10)
129
- });
130
- }
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>';
131
40
  }
132
- return frames;
41
+ html += '</div>';
42
+ return html;
133
43
  }
134
44
 
135
45
  function createOverlay() {
136
46
  __overlay = document.createElement('div');
137
- __overlay.id = '__jux-error-overlay';
138
- __overlay.setAttribute('style', [
139
- 'position:fixed',
140
- 'top:0',
141
- 'left:0',
142
- 'width:100vw',
143
- 'height:100vh',
144
- 'z-index:99999',
145
- 'background:rgba(0,0,0,0.85)',
146
- 'color:#f8f8f8',
147
- 'font-family:monospace',
148
- 'font-size:14px',
149
- 'overflow-y:auto',
150
- 'padding:0',
151
- 'margin:0',
152
- 'box-sizing:border-box'
153
- ].join(';'));
154
-
155
- var container = document.createElement('div');
156
- container.setAttribute('style', [
157
- 'max-width:960px',
158
- 'margin:40px auto',
159
- 'padding:24px 32px',
160
- 'border:2px solid #ff5555',
161
- 'border-radius:8px',
162
- 'background:#1a1a1a'
163
- ].join(';'));
164
-
165
- var header = document.createElement('div');
166
- header.setAttribute('style', [
167
- 'display:flex',
168
- 'justify-content:space-between',
169
- 'align-items:center',
170
- 'margin-bottom:16px',
171
- 'padding-bottom:12px',
172
- 'border-bottom:1px solid #333'
173
- ].join(';'));
174
-
175
- var title = document.createElement('div');
176
- title.setAttribute('style', 'color:#ff5555;font-size:16px;font-weight:bold;');
177
- title.textContent = 'Runtime Error';
178
- header.appendChild(title);
179
-
180
- var actions = document.createElement('div');
181
- actions.setAttribute('style', 'display:flex;gap:8px;');
182
-
183
- var clearBtn = document.createElement('button');
184
- clearBtn.textContent = 'Clear';
185
- clearBtn.setAttribute('style', [
186
- 'background:transparent',
187
- 'border:1px solid #555',
188
- 'color:#aaa',
189
- 'padding:4px 12px',
190
- 'border-radius:4px',
191
- 'cursor:pointer',
192
- 'font-family:monospace',
193
- 'font-size:12px'
194
- ].join(';'));
195
- clearBtn.addEventListener('click', clearErrors);
196
-
197
- var closeBtn = document.createElement('button');
198
- closeBtn.textContent = 'Close';
199
- closeBtn.setAttribute('style', [
200
- 'background:transparent',
201
- 'border:1px solid #555',
202
- 'color:#aaa',
203
- 'padding:4px 12px',
204
- 'border-radius:4px',
205
- 'cursor:pointer',
206
- 'font-family:monospace',
207
- 'font-size:12px'
208
- ].join(';'));
209
- closeBtn.addEventListener('click', function() {
210
- __overlay.style.display = 'none';
211
- });
212
-
213
- actions.appendChild(clearBtn);
214
- actions.appendChild(closeBtn);
215
- header.appendChild(actions);
216
- container.appendChild(header);
217
-
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);
218
60
  __list = document.createElement('div');
219
- container.appendChild(__list);
220
-
221
- __overlay.appendChild(container);
61
+ c.appendChild(__list); __overlay.appendChild(c);
222
62
  document.body.appendChild(__overlay);
223
63
  }
224
64
 
225
- function clearErrors() {
226
- __errors = [];
227
- if (__list) __list.innerHTML = '';
228
- if (__overlay) __overlay.style.display = 'none';
229
- }
230
-
231
- function renderSourceContext(resolved) {
232
- if (!resolved) return '';
233
-
234
- var lines = resolved.context || [];
235
- var startLine = resolved.contextStartLine || 1;
236
- var highlight = resolved.highlightLine || -1;
237
-
238
- var html = '<div style="margin-top:8px;padding:8px;background:#111;border:1px solid #333;border-radius:4px;">';
239
- html += '<div style="color:#ff5555;font-size:12px;margin-bottom:6px;font-weight:bold;">' +
240
- escapeHtml(resolved.file) + ':' + resolved.originalLine +
241
- '</div>';
242
-
243
- for (var i = 0; i < lines.length; i++) {
244
- var lineNum = startLine + i;
245
- var isHighlight = lineNum === highlight;
246
- var bg = isHighlight ? 'background:#3a1a1a;' : '';
247
- var color = isHighlight ? 'color:#ff8888;' : 'color:#888;';
248
- var numColor = isHighlight ? 'color:#ff5555;' : 'color:#555;';
249
- var marker = isHighlight ? '>' : ' ';
250
-
251
- html += '<div style="' + bg + 'padding:1px 8px;white-space:pre;">' +
252
- '<span style="' + numColor + 'display:inline-block;width:40px;text-align:right;margin-right:12px;user-select:none;">' +
253
- marker + ' ' + lineNum +
254
- '</span>' +
255
- '<span style="' + color + '">' + escapeHtml(lines[i]) + '</span>' +
256
- '</div>';
257
- }
258
-
259
- html += '</div>';
260
- return html;
261
- }
262
-
263
- function addError(type, message, source, line, col, stack) {
264
- if (__errors.length >= __maxErrors) __errors.shift();
265
-
266
- var entry = {
267
- type: type,
268
- message: String(message || ''),
269
- source: source || '',
270
- line: line || 0,
271
- col: col || 0,
272
- stack: stack || '',
273
- time: new Date().toLocaleTimeString()
274
- };
275
- __errors.push(entry);
276
-
65
+ function addError(type, message, stack) {
66
+ if (__errors.length >= ${maxErrors}) __errors.shift();
67
+ __errors.push({ type: type, message: message, stack: stack });
277
68
  if (!__overlay) createOverlay();
278
69
  __overlay.style.display = 'block';
279
70
 
280
71
  var item = document.createElement('div');
281
- item.setAttribute('style', [
282
- 'padding:12px 16px',
283
- 'margin-bottom:8px',
284
- 'background:#222',
285
- 'border-left:3px solid #ff5555',
286
- 'border-radius:4px'
287
- ].join(';'));
72
+ item.setAttribute('style','padding:12px 16px;margin-bottom:8px;background:#222;border-left:3px solid #ff5555;border-radius:4px;');
288
73
 
289
- 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>';
290
77
 
291
- item.innerHTML =
292
- '<div style="color:#888;font-size:12px;margin-bottom:4px;">' +
293
- typeLabel +
294
- '<span style="float:right;">' + entry.time + '</span>' +
295
- '</div>' +
296
- '<div style="color:#ff8888;font-size:14px;white-space:pre-wrap;word-break:break-word;font-weight:bold;">' +
297
- escapeHtml(entry.message) +
298
- '</div>';
78
+ var src = findSource(stack);
79
+ if (src) item.innerHTML += renderSource(src);
299
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
+ }
300
88
  __list.appendChild(item);
301
-
302
- // Resolve source location asynchronously
303
- loadSources(function() {
304
- var frames = parseStack(entry.stack);
305
- var resolved = null;
306
-
307
- // Try each frame until we find one that maps to a .jux source
308
- for (var i = 0; i < frames.length; i++) {
309
- var r = resolveSourceLocation(frames[i].line);
310
- if (r) {
311
- resolved = r;
312
- break;
313
- }
314
- }
315
-
316
- // Also try the direct line/col from the error itself
317
- if (!resolved && entry.line) {
318
- resolved = resolveSourceLocation(entry.line);
319
- }
320
-
321
- if (resolved) {
322
- // Add source context block
323
- var sourceBlock = document.createElement('div');
324
- sourceBlock.innerHTML = renderSourceContext(resolved);
325
- item.appendChild(sourceBlock);
326
- }
327
-
328
- // Add raw stack (collapsed) if present
329
- if (entry.stack) {
330
- var stackBlock = document.createElement('details');
331
- stackBlock.setAttribute('style', 'margin-top:8px;');
332
- stackBlock.innerHTML =
333
- '<summary style="cursor:pointer;color:#555;font-size:12px;user-select:none;">Raw stack trace</summary>' +
334
- '<pre style="margin:4px 0 0;padding:8px;font-size:12px;color:#666;' +
335
- 'white-space:pre-wrap;background:#111;border-radius:4px;' +
336
- 'border:1px solid #333;max-height:200px;overflow-y:auto;">' +
337
- escapeHtml(entry.stack) +
338
- '</pre>';
339
- item.appendChild(stackBlock);
340
- }
341
- });
342
89
  }
343
90
 
344
- function escapeHtml(str) {
345
- return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
346
- }
347
-
348
- // --- Hooks ---
349
-
350
- window.onerror = function(msg, source, line, col, err) {
351
- 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 : '');
352
93
  };
353
-
354
94
  window.addEventListener('unhandledrejection', function(e) {
355
- var reason = e.reason;
356
- var msg = reason instanceof Error ? reason.message : String(reason);
357
- var stack = reason instanceof Error ? reason.stack : '';
358
- 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 : '');
359
97
  });
360
-
361
- var _origConsoleError = console.error;
98
+ var _ce = console.error;
362
99
  console.error = function() {
363
- var args = Array.prototype.slice.call(arguments);
364
- var msg = args.map(function(a) {
365
- 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);
366
102
  }).join(' ');
367
- addError('console', msg);
368
- _origConsoleError.apply(console, arguments);
369
- };
370
-
371
- window.__juxErrors = {
372
- list: function() { return __errors.slice(); },
373
- clear: clearErrors,
374
- count: function() { return __errors.length; }
103
+ addError('console', msg, '');
104
+ _ce.apply(console, arguments);
375
105
  };
106
+ window.__juxErrors = { list: function() { return __errors.slice(); }, count: function() { return __errors.length; } };
376
107
  })();
377
108
  </script>`;
378
109
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.156",
3
+ "version": "1.1.158",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",