juxscript 1.1.153 → 1.1.155

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.
@@ -508,6 +508,12 @@ export class JuxCompiler {
508
508
  fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
509
509
  console.log(`📸 Source snapshot written`);
510
510
 
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
+
511
517
  const validation = this.reportValidationIssues();
512
518
  if (!validation.isValid) {
513
519
  console.log('🛑 BUILD FAILED\n');
@@ -1,19 +1,17 @@
1
1
  /**
2
2
  * Generates an injectable client-side error collector script.
3
- * Catches runtime errors, unhandled promise rejections, and console.error calls.
4
- * Displays them in a floating overlay panel in the browser.
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
5
  *
6
6
  * @param {Object} options
7
- * @param {boolean} [options.enabled=true] - Whether to enable the collector
8
- * @param {number} [options.maxErrors=50] - Max errors to keep in buffer
9
- * @param {string} [options.position='bottom-right'] - Panel position
7
+ * @param {boolean} [options.enabled=true]
8
+ * @param {number} [options.maxErrors=50]
10
9
  * @returns {string} Inline script to inject into HTML
11
10
  */
12
11
  export function generateErrorCollector(options = {}) {
13
12
  const {
14
13
  enabled = true,
15
- maxErrors = 50,
16
- position = 'bottom-right'
14
+ maxErrors = 50
17
15
  } = options;
18
16
 
19
17
  if (!enabled) return '';
@@ -21,93 +19,249 @@ export function generateErrorCollector(options = {}) {
21
19
  return `
22
20
  <script>
23
21
  (function() {
24
- var __juxErrors = [];
25
- var __juxMaxErrors = ${maxErrors};
26
- var __juxPanel = null;
27
- var __juxList = null;
28
- var __juxBadge = null;
29
- var __juxVisible = false;
30
-
31
- var posMap = {
32
- 'bottom-right': 'bottom:12px;right:12px;',
33
- 'bottom-left': 'bottom:12px;left:12px;',
34
- 'top-right': 'top:12px;right:12px;',
35
- 'top-left': 'top:12px;left:12px;'
36
- };
37
- var panelPos = posMap['${position}'] || posMap['bottom-right'];
38
-
39
- function createPanel() {
40
- // Toggle button / badge
41
- __juxBadge = document.createElement('div');
42
- __juxBadge.id = '__jux-error-badge';
43
- __juxBadge.setAttribute('style',
44
- 'position:fixed;' + panelPos +
45
- 'z-index:99999;width:36px;height:36px;border-radius:50%;' +
46
- 'background:#e74c3c;color:#fff;font-size:1rem;font-weight:bold;' +
47
- 'display:flex;align-items:center;justify-content:center;cursor:pointer;' +
48
- 'font-family:monospace;box-shadow:0 2px 8px rgba(0,0,0,0.3);'
49
- );
50
- __juxBadge.textContent = '0';
51
- __juxBadge.addEventListener('click', togglePanel);
52
- document.body.appendChild(__juxBadge);
53
-
54
- // Panel
55
- __juxPanel = document.createElement('div');
56
- __juxPanel.id = '__jux-error-panel';
57
- __juxPanel.setAttribute('style',
58
- 'position:fixed;bottom:56px;right:12px;z-index:99998;' +
59
- 'width:480px;max-height:360px;background:#1e1e1e;color:#f8f8f8;' +
60
- 'border:1px solid #e74c3c;border-radius:8px;font-family:monospace;' +
61
- 'font-size:1rem;display:flex;flex-direction:column;' +
62
- 'box-shadow:0 4px 16px rgba(0,0,0,0.4);'
63
- );
64
-
65
- // Header
22
+ var __errors = [];
23
+ var __maxErrors = ${maxErrors};
24
+ var __overlay = null;
25
+ 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
+
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
+ }
77
+ }
78
+
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
+ };
113
+ }
114
+
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');
120
+ 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
+ }
131
+ }
132
+ return frames;
133
+ }
134
+
135
+ function createOverlay() {
136
+ __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
+
66
165
  var header = document.createElement('div');
67
- header.setAttribute('style',
68
- 'padding:8px 12px;background:#e74c3c;color:#fff;font-weight:bold;' +
69
- 'display:flex;justify-content:space-between;align-items:center;' +
70
- 'border-radius:7px 7px 0 0;'
71
- );
72
- header.innerHTML = '<span>⚠ Runtime Errors</span>';
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;');
73
182
 
74
183
  var clearBtn = document.createElement('button');
75
184
  clearBtn.textContent = 'Clear';
76
- clearBtn.setAttribute('style',
77
- 'background:transparent;border:1px solid #fff;color:#fff;' +
78
- 'padding:2px 8px;border-radius:4px;cursor:pointer;font-size:1rem;'
79
- );
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(';'));
80
195
  clearBtn.addEventListener('click', clearErrors);
81
- header.appendChild(clearBtn);
82
196
 
83
- __juxPanel.appendChild(header);
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
+ });
84
212
 
85
- // List
86
- __juxList = document.createElement('div');
87
- __juxList.setAttribute('style',
88
- 'overflow-y:auto;max-height:300px;padding:8px;'
89
- );
90
- __juxPanel.appendChild(__juxList);
213
+ actions.appendChild(clearBtn);
214
+ actions.appendChild(closeBtn);
215
+ header.appendChild(actions);
216
+ container.appendChild(header);
91
217
 
92
- document.body.appendChild(__juxPanel);
93
- }
218
+ __list = document.createElement('div');
219
+ container.appendChild(__list);
94
220
 
95
- function togglePanel() {
96
- __juxVisible = !__juxVisible;
97
- __juxPanel.style.display = __juxVisible ? 'flex' : 'none';
221
+ __overlay.appendChild(container);
222
+ document.body.appendChild(__overlay);
98
223
  }
99
224
 
100
225
  function clearErrors() {
101
- __juxErrors = [];
102
- __juxList.innerHTML = '';
103
- __juxBadge.textContent = '0';
104
- __juxBadge.style.display = 'none';
105
- __juxPanel.style.display = 'none';
106
- __juxVisible = false;
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;
107
261
  }
108
262
 
109
263
  function addError(type, message, source, line, col, stack) {
110
- if (__juxErrors.length >= __juxMaxErrors) __juxErrors.shift();
264
+ if (__errors.length >= __maxErrors) __errors.shift();
111
265
 
112
266
  var entry = {
113
267
  type: type,
@@ -118,39 +272,73 @@ export function generateErrorCollector(options = {}) {
118
272
  stack: stack || '',
119
273
  time: new Date().toLocaleTimeString()
120
274
  };
121
- __juxErrors.push(entry);
275
+ __errors.push(entry);
122
276
 
123
- if (!__juxPanel) createPanel();
124
-
125
- // Update badge
126
- __juxBadge.textContent = String(__juxErrors.length);
127
- __juxBadge.style.display = 'flex';
128
-
129
- // ✅ Auto-open panel on first error
130
- if (!__juxVisible) {
131
- __juxVisible = true;
132
- __juxPanel.style.display = 'flex';
133
- }
277
+ if (!__overlay) createOverlay();
278
+ __overlay.style.display = 'block';
134
279
 
135
- // Add to list
136
280
  var item = document.createElement('div');
137
- item.setAttribute('style',
138
- 'padding:6px 8px;margin-bottom:4px;background:#2d2d2d;border-radius:4px;' +
139
- 'border-left:3px solid ' + (type === 'error' ? '#e74c3c' : type === 'rejection' ? '#e67e22' : '#f39c12') + ';'
140
- );
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(';'));
141
288
 
142
- var label = type === 'error' ? 'Error' : type === 'rejection' ? 'Rejection' : 'Console';
289
+ var typeLabel = type === 'error' ? 'Error' : type === 'rejection' ? 'Unhandled Rejection' : 'console.error';
143
290
 
144
291
  item.innerHTML =
145
- '<div style="color:#888;font-size:1rem;margin-bottom:2px;">' +
146
- entry.time + ' • ' + label +
147
- (entry.source ? ' ' + entry.source + ':' + entry.line : '') +
292
+ '<div style="color:#888;font-size:12px;margin-bottom:4px;">' +
293
+ typeLabel +
294
+ '<span style="float:right;">' + entry.time + '</span>' +
148
295
  '</div>' +
149
- '<div style="word-break:break-word;">' + escapeHtml(entry.message) + '</div>' +
150
- (entry.stack ? '<div style="margin-top:4px;"><div style="cursor:pointer;color:#888;font-size:1rem;">Stack trace</div><pre style="margin:4px 0 0;font-size:1rem;color:#aaa;white-space:pre-wrap;">' + escapeHtml(entry.stack) + '</pre></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>';
299
+
300
+ __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
+ }
151
327
 
152
- __juxList.appendChild(item);
153
- __juxList.scrollTop = __juxList.scrollHeight;
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
+ });
154
342
  }
155
343
 
156
344
  function escapeHtml(str) {
@@ -159,12 +347,10 @@ export function generateErrorCollector(options = {}) {
159
347
 
160
348
  // --- Hooks ---
161
349
 
162
- // 1. window.onerror
163
350
  window.onerror = function(msg, source, line, col, err) {
164
351
  addError('error', msg, source, line, col, err && err.stack ? err.stack : '');
165
352
  };
166
353
 
167
- // 2. Unhandled promise rejections
168
354
  window.addEventListener('unhandledrejection', function(e) {
169
355
  var reason = e.reason;
170
356
  var msg = reason instanceof Error ? reason.message : String(reason);
@@ -172,7 +358,6 @@ export function generateErrorCollector(options = {}) {
172
358
  addError('rejection', msg, '', 0, 0, stack);
173
359
  });
174
360
 
175
- // 3. Intercept console.error
176
361
  var _origConsoleError = console.error;
177
362
  console.error = function() {
178
363
  var args = Array.prototype.slice.call(arguments);
@@ -183,11 +368,10 @@ export function generateErrorCollector(options = {}) {
183
368
  _origConsoleError.apply(console, arguments);
184
369
  };
185
370
 
186
- // Expose API for programmatic access
187
371
  window.__juxErrors = {
188
- list: function() { return __juxErrors.slice(); },
372
+ list: function() { return __errors.slice(); },
189
373
  clear: clearErrors,
190
- count: function() { return __juxErrors.length; }
374
+ count: function() { return __errors.length; }
191
375
  };
192
376
  })();
193
377
  </script>`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.153",
3
+ "version": "1.1.155",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",