juxscript 1.1.129 → 1.1.131

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "totalComponents": 69,
3
- "generatedAt": "2026-02-13T05:40:44.264Z",
3
+ "generatedAt": "2026-02-13T05:56:39.490Z",
4
4
  "components": [
5
5
  {
6
6
  "file": "alert.js",
@@ -1,25 +1,66 @@
1
+ import * as esbuild from 'esbuild';
2
+ import * as acorn from 'acorn';
3
+ import { walk } from 'astray';
1
4
  import fs from 'fs';
2
5
  import path from 'path';
3
- import esbuild from 'esbuild';
6
+ import { fileURLToPath } from 'url';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
4
10
 
5
11
  export class JuxCompiler {
6
- constructor(config) {
12
+ constructor(config = {}) {
7
13
  this.config = config;
8
- this.srcDir = config.srcDir;
9
- this.distDir = config.distDir;
10
- this.publicDir = config.publicDir;
11
- this.paths = config.paths;
14
+ this.srcDir = config.srcDir || './jux';
15
+ this.distDir = config.distDir || './.jux-dist';
16
+ this.publicDir = config.publicDir || './public'; // ✅ Configurable public path
17
+ this.defaults = config.defaults || {};
18
+ this.paths = config.paths || {};
19
+ this._juxscriptExports = null;
20
+ this._juxscriptPath = null;
12
21
  }
13
22
 
14
- /* ═══════════════════════════════════════════════════════════════════
15
- * SCAN FILES (Recursive)
16
- * ═══════════════════════════════════════════════════════════════════ */
23
+ /**
24
+ * Locate juxscript package - simplified resolution
25
+ */
26
+ findJuxscriptPath() {
27
+ if (this._juxscriptPath) return this._juxscriptPath;
28
+
29
+ const projectRoot = process.cwd();
30
+
31
+ // Priority 1: User's node_modules (when used as dependency)
32
+ const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
33
+ if (fs.existsSync(userPath)) {
34
+ this._juxscriptPath = userPath;
35
+ return userPath;
36
+ }
37
+
38
+ // Priority 2: Package root (when developing juxscript itself)
39
+ const packageRoot = path.resolve(__dirname, '..');
40
+ const devPath = path.resolve(packageRoot, 'index.js');
41
+ if (fs.existsSync(devPath)) {
42
+ this._juxscriptPath = devPath;
43
+ return devPath;
44
+ }
45
+
46
+ // Priority 3: Sibling in monorepo
47
+ const monoPath = path.resolve(projectRoot, '../jux/index.js');
48
+ if (fs.existsSync(monoPath)) {
49
+ this._juxscriptPath = monoPath;
50
+ return monoPath;
51
+ }
52
+
53
+ return null;
54
+ }
17
55
 
56
+ /**
57
+ * ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
58
+ */
18
59
  scanFiles() {
19
60
  const views = [], dataModules = [], sharedModules = [];
20
61
 
21
62
  /**
22
- * Recursive directory scanner
63
+ * Recursive directory scanner
23
64
  */
24
65
  const scanDirectory = (currentDir) => {
25
66
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
@@ -58,37 +99,146 @@ export class JuxCompiler {
58
99
  }
59
100
 
60
101
  isAssetFile(filename) {
61
- const assetExtensions = ['.css', '.scss', '.sass', '.less', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.otf'];
102
+ const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
62
103
  return assetExtensions.some(ext => filename.endsWith(ext));
63
104
  }
64
105
 
65
- /* ═══════════════════════════════════════════════════════════════════
66
- * GENERATE ENTRY POINT
67
- * ═══════════════════════════════════════════════════════════════════ */
106
+ removeImports(code) {
107
+ return code
108
+ .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
109
+ .replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
110
+ .replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
111
+ .replace(/^\s*import\s*;?\s*$/gm, '');
112
+ }
113
+
114
+ sanitizeName(name) {
115
+ return name.replace(/[^a-zA-Z0-9]/g, '_');
116
+ }
117
+
118
+ async loadJuxscriptExports() {
119
+ if (this._juxscriptExports) return this._juxscriptExports;
68
120
 
121
+ try {
122
+ const juxscriptPath = this.findJuxscriptPath();
123
+ if (juxscriptPath) {
124
+ const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
125
+ const exports = new Set();
126
+
127
+ for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
128
+ match[1].split(',').forEach(exp => {
129
+ const name = exp.trim().split(/\s+as\s+/)[0].trim();
130
+ if (name) exports.add(name);
131
+ });
132
+ }
133
+
134
+ this._juxscriptExports = [...exports];
135
+ if (this._juxscriptExports.length > 0) {
136
+ console.log(`📦 juxscript exports: ${this._juxscriptExports.join(', ')}`);
137
+ }
138
+ }
139
+ } catch (err) {
140
+ this._juxscriptExports = [];
141
+ }
142
+
143
+ return this._juxscriptExports;
144
+ }
145
+
146
+ validateViewCode(viewName, code) {
147
+ const issues = [];
148
+
149
+ let ast;
150
+ try {
151
+ ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
152
+ } catch (parseError) {
153
+ issues.push({
154
+ type: 'error',
155
+ view: viewName,
156
+ line: parseError.loc?.line || 0,
157
+ message: `Syntax error: ${parseError.message}`,
158
+ code: ''
159
+ });
160
+ return issues;
161
+ }
162
+
163
+ const allImports = new Set();
164
+
165
+ walk(ast, {
166
+ ImportDeclaration(node) {
167
+ node.specifiers.forEach(spec => {
168
+ allImports.add(spec.local.name);
169
+ });
170
+ }
171
+ });
172
+
173
+ // Default known facades/components if load fails
174
+ const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
175
+
176
+ walk(ast, {
177
+ Identifier(node, parent) {
178
+ if (parent?.type === 'CallExpression' && parent.callee === node) {
179
+ const name = node.name;
180
+ if (!allImports.has(name) && knownComponents.includes(name)) {
181
+ issues.push({
182
+ type: 'warning',
183
+ view: viewName,
184
+ line: node.loc?.start?.line || 0,
185
+ message: `"${name}" is used but not imported from juxscript`,
186
+ code: ''
187
+ });
188
+ }
189
+ }
190
+ }
191
+ });
192
+
193
+ return issues;
194
+ }
195
+
196
+ /**
197
+ * Generate entry point without layout/theme logic
198
+ */
69
199
  generateEntryPoint(views, dataModules, sharedModules) {
200
+ let entry = `// Auto-generated JUX entry point\n\n`;
201
+ const allIssues = [];
70
202
  const sourceSnapshot = {};
71
- let entry = `/* Auto-generated entry point */\n`;
72
- entry += `import { jux, state, registry } from 'juxscript';\n\n`;
73
203
 
74
- // Import shared modules
75
- sharedModules.forEach(m => {
76
- entry += `// Shared: ${m.file}\n${m.content}\n\n`;
204
+ const juxImports = new Set();
205
+ [...views, ...dataModules, ...sharedModules].forEach(m => {
206
+ for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
207
+ match[1].split(',').map(s => s.trim()).forEach(imp => {
208
+ if (imp) juxImports.add(imp);
209
+ });
210
+ }
77
211
  });
78
212
 
79
- // Import data modules
80
- dataModules.forEach(d => {
81
- entry += `// Data: ${d.file}\n${d.content}\n\n`;
213
+ if (juxImports.size > 0) {
214
+ entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
215
+ }
216
+
217
+ dataModules.forEach(m => {
218
+ entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
219
+ });
220
+ sharedModules.forEach(m => {
221
+ entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
82
222
  });
83
223
 
84
- // Generate render functions for views
224
+ entry += `\n// Expose to window\n`;
225
+ dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
226
+ sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
227
+
228
+ if (juxImports.size > 0) {
229
+ entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
230
+ }
231
+
232
+ entry += `\n// --- VIEW FUNCTIONS ---\n`;
233
+
85
234
  views.forEach(v => {
86
235
  // ✅ Sanitize the name for use in function names
236
+ // Replace slashes, dots, and other invalid characters with underscores
87
237
  const sanitizedName = v.name
88
- .replace(/[\/\\.\\-\s]/g, '_')
89
- .replace(/[^a-zA-Z0-9_]/g, '_')
90
- .replace(/_+/g, '_')
91
- .replace(/^_|_$/g, '');
238
+ .replace(/[\/\\.\\-\s]/g, '_') // Replace /, \, ., -, spaces with _
239
+ .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
240
+ .replace(/_+/g, '_') // Collapse multiple consecutive underscores
241
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
92
242
 
93
243
  const capitalized = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
94
244
 
@@ -102,12 +252,44 @@ export class JuxCompiler {
102
252
  let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
103
253
  const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
104
254
 
255
+ // ✅ Use sanitized name in function declaration
105
256
  entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
106
257
  });
107
258
 
108
- // Generate route map with sanitized names
259
+ dataModules.forEach(m => {
260
+ sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
261
+ });
262
+ sharedModules.forEach(m => {
263
+ sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
264
+ });
265
+
266
+ this._sourceSnapshot = sourceSnapshot;
267
+ this._validationIssues = allIssues;
268
+ entry += this._generateRouter(views);
269
+ return entry;
270
+ }
271
+
272
+ reportValidationIssues() {
273
+ const issues = this._validationIssues || [];
274
+ const errors = issues.filter(i => i.type === 'error');
275
+ const warnings = issues.filter(i => i.type === 'warning');
276
+
277
+ if (issues.length > 0) {
278
+ console.log('\n⚠️ Validation Issues:\n');
279
+ issues.forEach(issue => {
280
+ const icon = issue.type === 'error' ? '❌' : '⚠️';
281
+ console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
282
+ });
283
+ console.log('');
284
+ }
285
+
286
+ return { isValid: errors.length === 0, errors, warnings };
287
+ }
288
+
289
+ _generateRouter(views) {
109
290
  let routeMap = '';
110
291
  views.forEach(v => {
292
+ // ✅ Sanitize function name (same as above)
111
293
  const sanitizedName = v.name
112
294
  .replace(/[\/\\.\\-\s]/g, '_')
113
295
  .replace(/[^a-zA-Z0-9_]/g, '_')
@@ -116,93 +298,251 @@ export class JuxCompiler {
116
298
 
117
299
  const cap = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
118
300
 
119
- // ✅ Generate URL-safe route path
301
+ // ✅ Generate URL-safe route path from original name
302
+ // Convert: 'menus/main' → '/menus/main'
303
+ // Convert: 'about-us' → '/about-us'
304
+ // Convert: 'blog.post' → '/blog-post' (dots become dashes for URLs)
120
305
  const routePath = v.name
121
306
  .toLowerCase()
122
- .replace(/\\/g, '/')
123
- .replace(/\.jux$/i, '')
124
- .replace(/\./g, '-')
125
- .replace(/\s+/g, '-')
126
- .replace(/[^a-z0-9\/_-]/g, '')
127
- .replace(/-+/g, '-')
128
- .replace(/^-|-$/g, '');
129
-
307
+ .replace(/\\/g, '/') // Normalize backslashes to forward slashes
308
+ .replace(/\.jux$/i, '') // Remove .jux extension if present
309
+ .replace(/\./g, '-') // Convert dots to dashes (blog.post → blog-post)
310
+ .replace(/\s+/g, '-') // Convert spaces to dashes
311
+ .replace(/[^a-z0-9\/_-]/g, '') // Remove any other unsafe URL chars
312
+ .replace(/-+/g, '-') // Collapse multiple dashes
313
+ .replace(/^-|-$/g, ''); // Remove leading/trailing dashes
314
+
315
+ // ✅ Handle index route
130
316
  if (routePath === 'index' || routePath === '') {
131
317
  routeMap += ` '/': render${cap},\n`;
132
318
  }
133
319
 
320
+ // ✅ Add regular route
134
321
  routeMap += ` '/${routePath}': render${cap},\n`;
135
322
  });
136
323
 
137
- // Router setup
138
- entry += `\n// Router\nconst routes = {\n${routeMap}};\n\n`;
139
- entry += `function route(path) {\n`;
140
- entry += ` const renderFn = routes[path] || routes['/'];\n`;
141
- entry += ` if (renderFn) {\n`;
142
- entry += ` document.getElementById('app').innerHTML = '';\n`;
143
- entry += ` renderFn();\n`;
144
- entry += ` }\n`;
145
- entry += `}\n\n`;
146
- entry += `route(window.location.pathname);\n`;
147
- entry += `window.addEventListener('popstate', () => route(window.location.pathname));\n`;
148
-
149
- // Save source snapshot
150
- const snapshotPath = path.join(this.distDir, 'source-snapshot.json');
151
- fs.writeFileSync(snapshotPath, JSON.stringify(sourceSnapshot, null, 2));
152
- console.log('📸 Source snapshot written');
324
+ return `
325
+ // --- JUX SOURCE LOADER ---
326
+ var __juxSources = null;
327
+ async function __juxLoadSources() {
328
+ if (__juxSources) return __juxSources;
329
+ try {
330
+ var res = await fetch('/__jux_sources.json');
331
+ __juxSources = await res.json();
332
+ } catch (e) {
333
+ __juxSources = {};
334
+ }
335
+ return __juxSources;
336
+ }
153
337
 
154
- return entry;
338
+ function __juxFindSource(stack) {
339
+ var match = stack.match(/render(\\w+)/);
340
+ if (match) {
341
+ var viewName = match[1].toLowerCase();
342
+ for (var file in __juxSources || {}) {
343
+ if (__juxSources[file].name.toLowerCase() === viewName) {
344
+ return { file: file, source: __juxSources[file], viewName: match[1] };
345
+ }
346
+ }
155
347
  }
348
+ return null;
349
+ }
156
350
 
157
- removeImports(code) {
158
- return code.replace(/^\s*import\s+.*?from\s+['"].*?['"];?\s*$/gm, '');
351
+ // --- JUX RUNTIME ERROR OVERLAY ---
352
+ var __juxErrorOverlay = {
353
+ styles: \`
354
+ #__jux-error-overlay {
355
+ position: fixed; inset: 0; z-index: 99999;
356
+ background: rgba(0, 0, 0, 0.4);
357
+ display: flex; align-items: center; justify-content: center;
358
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
359
+ opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
360
+ }
361
+ #__jux-error-overlay.visible { opacity: 1; }
362
+ #__jux-error-overlay * { box-sizing: border-box; }
363
+ .__jux-modal {
364
+ background: #f8f9fa; border-radius: 4px;
365
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
366
+ max-width: 80vw; width: 90%; max-height: 90vh;
367
+ overflow: hidden; display: flex; flex-direction: column;
368
+ transform: translateY(10px); transition: transform 0.2s ease-out;
369
+ }
370
+ #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
371
+ .__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
372
+ .__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
373
+ .__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
374
+ .__jux-header .file-info { color: #6b7280; font-size: 13px; }
375
+ .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
376
+ .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
377
+ .__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
378
+ .__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
379
+ .__jux-code-line.error { background: #fef2f2; }
380
+ .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
381
+ .__jux-code-line.context { background: #fefce8; }
382
+ .__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
383
+ .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
384
+ .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
385
+ .__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
386
+ .__jux-tip { color: #6b7280; font-size: 12px; }
387
+ .__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
388
+ .__jux-dismiss:hover { background: #e5e7eb; }
389
+ .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
390
+ .__jux-no-source pre { background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px; font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left; border: 1px solid #e5e7eb; }
391
+ \`,
392
+
393
+ show: async function(error, title) {
394
+ title = title || 'Runtime Error';
395
+ var existing = document.getElementById('__jux-error-overlay');
396
+ if (existing) existing.remove();
397
+ await __juxLoadSources();
398
+
399
+ var overlay = document.createElement('div');
400
+ overlay.id = '__jux-error-overlay';
401
+ var stack = error && error.stack ? error.stack : '';
402
+ var msg = error && error.message ? error.message : String(error);
403
+ var found = __juxFindSource(stack);
404
+ var sourceHtml = '', fileInfo = '';
405
+
406
+ if (found && found.source && found.source.lines) {
407
+ var lines = found.source.lines;
408
+ fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
409
+ var errorLineIndex = -1;
410
+ var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
411
+ if (errorMethod) {
412
+ for (var i = 0; i < lines.length; i++) {
413
+ if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
414
+ }
415
+ }
416
+ if (errorLineIndex === -1) {
417
+ for (var i = 0; i < lines.length; i++) {
418
+ if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
419
+ }
420
+ }
421
+ var contextStart = Math.max(0, errorLineIndex - 3);
422
+ var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
423
+ if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
424
+
425
+ for (var i = contextStart; i <= contextEnd; i++) {
426
+ var isError = (i === errorLineIndex);
427
+ var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
428
+ var lineClass = isError ? ' error' : (isContext ? ' context' : '');
429
+ var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
430
+ sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
431
+ }
432
+ } else {
433
+ sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
434
+ }
435
+
436
+ overlay.innerHTML = '<style>' + this.styles + '</style><div class="__jux-modal"><div class="__jux-header"><h3>' + title + '</h3><h1>' + msg + '</h1>' + fileInfo + '</div><div class="__jux-source"><div class="__jux-code">' + sourceHtml + '</div></div><div class="__jux-footer"><span class="__jux-tip">💡 Fix the error and save to reload</span><button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button></div></div>';
437
+ document.body.appendChild(overlay);
438
+ requestAnimationFrame(function() { overlay.classList.add('visible'); });
439
+ console.error(title + ':', error);
159
440
  }
441
+ };
442
+
443
+ window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
444
+ window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
445
+
446
+ // --- JUX ROUTER ---
447
+ const routes = {\n${routeMap}};
448
+
449
+ // Simple router
450
+ function route(path) {
451
+ const renderFn = routes[path] || routes['/'];
452
+ if (renderFn) {
453
+ document.getElementById('app').innerHTML = '';
454
+ renderFn();
455
+ } else {
456
+ document.getElementById('app').innerHTML = '<h1>404 - Page Not Found</h1>';
457
+ }
458
+ }
160
459
 
161
- /* ═══════════════════════════════════════════════════════════════════
162
- * COPY PUBLIC ASSETS
163
- * ═══════════════════════════════════════════════════════════════════ */
460
+ // Initial route
461
+ route(window.location.pathname);
164
462
 
165
- async copyPublicAssets() {
166
- const publicPath = this.paths?.public || path.resolve(this.srcDir, '..', this.publicDir);
463
+ // Handle navigation
464
+ window.addEventListener('popstate', () => route(window.location.pathname));
167
465
 
168
- if (!fs.existsSync(publicPath)) {
169
- console.log(`ℹ️ No public folder found, skipping`);
170
- return;
466
+ // Intercept link clicks
467
+ document.addEventListener('click', (e) => {
468
+ if (e.target.matches('a[href]')) {
469
+ const href = e.target.getAttribute('href');
470
+ if (href.startsWith('/') && !href.startsWith('//')) {
471
+ e.preventDefault();
472
+ window.history.pushState({}, '', href);
473
+ route(href);
171
474
  }
475
+ }
476
+ });
477
+ `;
478
+ }
172
479
 
173
- console.log('📦 Copying public assets...');
480
+ async build() {
481
+ console.log('🚀 JUX Build\n');
174
482
 
175
- const copyRecursive = (src, dest) => {
176
- if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
483
+ const juxscriptPath = this.findJuxscriptPath();
484
+ if (!juxscriptPath) {
485
+ console.error('❌ Could not locate juxscript package');
486
+ return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
487
+ }
488
+ console.log(`📦 Using: ${juxscriptPath}`);
177
489
 
178
- const entries = fs.readdirSync(src, { withFileTypes: true });
490
+ await this.loadJuxscriptExports();
179
491
 
180
- for (const entry of entries) {
181
- const srcPath = path.join(src, entry.name);
182
- const destPath = path.join(dest, entry.name);
492
+ if (fs.existsSync(this.distDir)) {
493
+ fs.rmSync(this.distDir, { recursive: true, force: true });
494
+ }
495
+ fs.mkdirSync(this.distDir, { recursive: true });
183
496
 
184
- if (entry.isDirectory()) {
185
- copyRecursive(srcPath, destPath);
186
- } else {
187
- const ext = path.extname(entry.name).toLowerCase();
188
- // ✅ Skip .jux files (they're compiled, not copied)
189
- if (ext !== '.jux') {
190
- fs.copyFileSync(srcPath, destPath);
191
- }
192
- }
193
- }
194
- };
497
+ // ✅ Copy public folder if exists
498
+ this.copyPublicFolder();
195
499
 
196
- copyRecursive(publicPath, this.distDir);
197
- console.log('✅ Public assets copied');
198
- }
500
+ const { views, dataModules, sharedModules } = this.scanFiles();
501
+ console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
502
+
503
+ // Copy data/shared modules to dist
504
+ const juxDistDir = path.join(this.distDir, 'jux');
505
+ fs.mkdirSync(juxDistDir, { recursive: true });
506
+ [...dataModules, ...sharedModules].forEach(m => {
507
+ fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
508
+ });
199
509
 
200
- /* ═══════════════════════════════════════════════════════════════════
201
- * GENERATE index.html
202
- * ═══════════════════════════════════════════════════════════════════ */
510
+ const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
511
+ const entryPath = path.join(this.distDir, 'entry.js');
512
+ fs.writeFileSync(entryPath, entryContent);
203
513
 
204
- async generateIndexHtml() {
205
- console.log(' ✅ Generated index.html');
514
+ const snapshotPath = path.join(this.distDir, '__jux_sources.json');
515
+ fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
516
+ console.log(`📸 Source snapshot written`);
517
+
518
+ const validation = this.reportValidationIssues();
519
+ if (!validation.isValid) {
520
+ console.log('🛑 BUILD FAILED\n');
521
+ return { success: false, errors: validation.errors, warnings: validation.warnings };
522
+ }
523
+
524
+ try {
525
+ await esbuild.build({
526
+ entryPoints: [entryPath],
527
+ bundle: true,
528
+ outfile: path.join(this.distDir, 'bundle.js'),
529
+ format: 'esm',
530
+ platform: 'browser',
531
+ target: 'esnext',
532
+ sourcemap: true,
533
+ loader: { '.jux': 'js', '.css': 'empty' },
534
+ plugins: [{
535
+ name: 'juxscript-resolver',
536
+ setup: (build) => {
537
+ build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
538
+ }
539
+ }],
540
+ });
541
+ console.log('✅ esbuild complete');
542
+ } catch (err) {
543
+ console.error('❌ esbuild failed:', err);
544
+ return { success: false, errors: [{ message: err.message }], warnings: [] };
545
+ }
206
546
 
207
547
  const html = `<!DOCTYPE html>
208
548
  <html lang="en">
@@ -210,80 +550,115 @@ export class JuxCompiler {
210
550
  <meta charset="UTF-8">
211
551
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
212
552
  <title>JUX Application</title>
213
- <link rel="stylesheet" href="/styles.css">
214
- <script src="https://unpkg.com/lucide@latest"></script>
553
+ <script type="module" src="/bundle.js"></script>
554
+ <script src="/entry.js"></script>
215
555
  </head>
216
556
  <body>
217
557
  <div id="app"></div>
218
- <script src="/entry.js"></script>
219
558
  </body>
220
559
  </html>`;
221
560
 
222
- fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
223
- }
224
-
225
- /* ═══════════════════════════════════════════════════════════════════
226
- * BUILD
227
- * ═══════════════════════════════════════════════════════════════════ */
228
-
229
- async build() {
230
- try {
231
- // Clean dist
232
- if (fs.existsSync(this.distDir)) {
233
- fs.rmSync(this.distDir, { recursive: true, force: true });
234
- }
235
- fs.mkdirSync(this.distDir, { recursive: true });
561
+ fs.writeFileSync(
562
+ path.join(this.config.distDir, 'index.html'),
563
+ html,
564
+ 'utf8'
565
+ );
236
566
 
237
- // Copy public assets first
238
- await this.copyPublicAssets();
567
+ console.log(' ✅ Generated index.html');
568
+ return { success: true, errors: [], warnings: validation.warnings };
569
+ }
239
570
 
240
- // Scan files
241
- const { views, dataModules, sharedModules } = this.scanFiles();
242
- console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
571
+ /**
572
+ * Copy public folder contents to dist
573
+ */
574
+ copyPublicFolder() {
575
+ // ✅ Use configured public path or resolve from paths object
576
+ const publicSrc = this.paths.public
577
+ ? this.paths.public
578
+ : path.resolve(process.cwd(), this.publicDir);
579
+
580
+ if (!fs.existsSync(publicSrc)) {
581
+ return; // No public folder, skip
582
+ }
243
583
 
244
- // Generate entry point
245
- const entryCode = this.generateEntryPoint(views, dataModules, sharedModules);
246
- const entryPath = path.join(this.distDir, 'entry-temp.js');
247
- fs.writeFileSync(entryPath, entryCode, 'utf8');
584
+ console.log('📦 Copying public assets...');
248
585
 
249
- // Bundle with esbuild
250
- await esbuild.build({
251
- entryPoints: [entryPath],
252
- bundle: true,
253
- outfile: path.join(this.distDir, 'entry.js'),
254
- format: 'esm',
255
- platform: 'browser',
256
- target: 'es2020',
257
- minify: false,
258
- sourcemap: false
259
- });
586
+ try {
587
+ this._copyDirRecursive(publicSrc, this.distDir, 0);
588
+ console.log('✅ Public assets copied');
589
+ } catch (err) {
590
+ console.warn('⚠️ Error copying public folder:', err.message);
591
+ }
592
+ }
260
593
 
261
- console.log('✅ esbuild complete');
594
+ /**
595
+ * Recursively copy directory contents
596
+ */
597
+ _copyDirRecursive(src, dest, depth = 0) {
598
+ const entries = fs.readdirSync(src, { withFileTypes: true });
262
599
 
263
- // Clean up temp file
264
- fs.unlinkSync(entryPath);
600
+ entries.forEach(entry => {
601
+ // Skip hidden files and directories
602
+ if (entry.name.startsWith('.')) return;
265
603
 
266
- // Generate index.html
267
- await this.generateIndexHtml();
604
+ const srcPath = path.join(src, entry.name);
605
+ const destPath = path.join(dest, entry.name);
268
606
 
269
- // Return success object
270
- return {
271
- success: true,
272
- errors: [],
273
- warnings: [],
274
- fileCount: views.length
275
- };
607
+ if (entry.isDirectory()) {
608
+ // Create directory and recurse
609
+ if (!fs.existsSync(destPath)) {
610
+ fs.mkdirSync(destPath, { recursive: true });
611
+ }
612
+ this._copyDirRecursive(srcPath, destPath, depth + 1);
613
+ } else {
614
+ // Copy file
615
+ fs.copyFileSync(srcPath, destPath);
616
+
617
+ // Log files at root level only
618
+ if (depth === 0) {
619
+ const ext = path.extname(entry.name);
620
+ const icon = this._getFileIcon(ext);
621
+ console.log(` ${icon} ${entry.name}`);
622
+ }
623
+ }
624
+ });
625
+ }
276
626
 
277
- } catch (error) {
278
- console.error('\n❌ Build failed:', error.message);
279
- console.error(error.stack);
627
+ /**
628
+ * Get icon for file type
629
+ */
630
+ _getFileIcon(ext) {
631
+ const icons = {
632
+ '.html': '📄',
633
+ '.css': '🎨',
634
+ '.js': '📜',
635
+ '.json': '📋',
636
+ '.png': '🖼️',
637
+ '.jpg': '🖼️',
638
+ '.jpeg': '🖼️',
639
+ '.gif': '🖼️',
640
+ '.svg': '🎨',
641
+ '.ico': '🔖',
642
+ '.woff': '🔤',
643
+ '.woff2': '🔤',
644
+ '.ttf': '🔤',
645
+ '.eot': '🔤'
646
+ };
647
+ return icons[ext.toLowerCase()] || '📦';
648
+ }
280
649
 
281
- // ✅ Return failure object
282
- return {
283
- success: false,
284
- errors: [error.message],
285
- warnings: []
286
- };
287
- }
650
+ /**
651
+ * ✅ Generate valid JavaScript identifier from file path
652
+ * Example: abc/aaa.jux -> abc_aaa
653
+ * Example: menus/main.jux -> menus_main
654
+ * Example: pages/blog/post.jux -> pages_blog_post
655
+ */
656
+ _generateNameFromPath(filepath) {
657
+ return filepath
658
+ .replace(/\.jux$/, '') // Remove .jux extension
659
+ .replace(/[\/\\]/g, '_') // Replace / and \ with _
660
+ .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
661
+ .replace(/_+/g, '_') // Collapse multiple consecutive underscores
662
+ .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
288
663
  }
289
664
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.129",
3
+ "version": "1.1.131",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",