juxscript 1.1.127 → 1.1.129

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:34:47.501Z",
3
+ "generatedAt": "2026-02-13T05:40:44.264Z",
4
4
  "components": [
5
5
  {
6
6
  "file": "alert.js",
@@ -1,66 +1,25 @@
1
- import * as esbuild from 'esbuild';
2
- import * as acorn from 'acorn';
3
- import { walk } from 'astray';
4
1
  import fs from 'fs';
5
2
  import path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
3
+ import esbuild from 'esbuild';
10
4
 
11
5
  export class JuxCompiler {
12
- constructor(config = {}) {
6
+ constructor(config) {
13
7
  this.config = config;
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;
8
+ this.srcDir = config.srcDir;
9
+ this.distDir = config.distDir;
10
+ this.publicDir = config.publicDir;
11
+ this.paths = config.paths;
21
12
  }
22
13
 
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
- }
14
+ /* ═══════════════════════════════════════════════════════════════════
15
+ * SCAN FILES (Recursive)
16
+ * ═══════════════════════════════════════════════════════════════════ */
55
17
 
56
- /**
57
- * ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
58
- */
59
18
  scanFiles() {
60
19
  const views = [], dataModules = [], sharedModules = [];
61
20
 
62
21
  /**
63
- * Recursive directory scanner
22
+ * Recursive directory scanner
64
23
  */
65
24
  const scanDirectory = (currentDir) => {
66
25
  const entries = fs.readdirSync(currentDir, { withFileTypes: true });
@@ -99,146 +58,37 @@ export class JuxCompiler {
99
58
  }
100
59
 
101
60
  isAssetFile(filename) {
102
- const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
61
+ const assetExtensions = ['.css', '.scss', '.sass', '.less', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.otf'];
103
62
  return assetExtensions.some(ext => filename.endsWith(ext));
104
63
  }
105
64
 
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;
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
- }
65
+ /* ═══════════════════════════════════════════════════════════════════
66
+ * GENERATE ENTRY POINT
67
+ * ═══════════════════════════════════════════════════════════════════ */
195
68
 
196
- /**
197
- * Generate entry point without layout/theme logic
198
- */
199
69
  generateEntryPoint(views, dataModules, sharedModules) {
200
- let entry = `// Auto-generated JUX entry point\n\n`;
201
- const allIssues = [];
202
70
  const sourceSnapshot = {};
71
+ let entry = `/* Auto-generated entry point */\n`;
72
+ entry += `import { jux, state, registry } from 'juxscript';\n\n`;
203
73
 
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
- }
211
- });
212
-
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
- });
74
+ // Import shared modules
220
75
  sharedModules.forEach(m => {
221
- entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
76
+ entry += `// Shared: ${m.file}\n${m.content}\n\n`;
222
77
  });
223
78
 
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`;
79
+ // Import data modules
80
+ dataModules.forEach(d => {
81
+ entry += `// Data: ${d.file}\n${d.content}\n\n`;
82
+ });
233
83
 
84
+ // Generate render functions for views
234
85
  views.forEach(v => {
235
86
  // ✅ Sanitize the name for use in function names
236
- // Replace slashes, dots, and other invalid characters with underscores
237
87
  const sanitizedName = v.name
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
88
+ .replace(/[\/\\.\\-\s]/g, '_')
89
+ .replace(/[^a-zA-Z0-9_]/g, '_')
90
+ .replace(/_+/g, '_')
91
+ .replace(/^_|_$/g, '');
242
92
 
243
93
  const capitalized = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
244
94
 
@@ -252,44 +102,12 @@ export class JuxCompiler {
252
102
  let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
253
103
  const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
254
104
 
255
- // ✅ Use sanitized name in function declaration
256
105
  entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
257
106
  });
258
107
 
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) {
108
+ // Generate route map with sanitized names
290
109
  let routeMap = '';
291
110
  views.forEach(v => {
292
- // ✅ Sanitize function name (same as above)
293
111
  const sanitizedName = v.name
294
112
  .replace(/[\/\\.\\-\s]/g, '_')
295
113
  .replace(/[^a-zA-Z0-9_]/g, '_')
@@ -298,251 +116,93 @@ export class JuxCompiler {
298
116
 
299
117
  const cap = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
300
118
 
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)
119
+ // ✅ Generate URL-safe route path
305
120
  const routePath = v.name
306
121
  .toLowerCase()
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
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
+
316
130
  if (routePath === 'index' || routePath === '') {
317
131
  routeMap += ` '/': render${cap},\n`;
318
132
  }
319
133
 
320
- // ✅ Add regular route
321
134
  routeMap += ` '/${routePath}': render${cap},\n`;
322
135
  });
323
136
 
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
- }
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');
337
153
 
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
- }
154
+ return entry;
347
155
  }
348
- return null;
349
- }
350
156
 
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);
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>';
157
+ removeImports(code) {
158
+ return code.replace(/^\s*import\s+.*?from\s+['"].*?['"];?\s*$/gm, '');
457
159
  }
458
- }
459
-
460
- // Initial route
461
- route(window.location.pathname);
462
160
 
463
- // Handle navigation
464
- window.addEventListener('popstate', () => route(window.location.pathname));
465
-
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);
474
- }
475
- }
476
- });
477
- `;
478
- }
161
+ /* ═══════════════════════════════════════════════════════════════════
162
+ * COPY PUBLIC ASSETS
163
+ * ═══════════════════════════════════════════════════════════════════ */
479
164
 
480
- async build() {
481
- console.log('🚀 JUX Build\n');
165
+ async copyPublicAssets() {
166
+ const publicPath = this.paths?.public || path.resolve(this.srcDir, '..', this.publicDir);
482
167
 
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: [] };
168
+ if (!fs.existsSync(publicPath)) {
169
+ console.log(`ℹ️ No public folder found, skipping`);
170
+ return;
487
171
  }
488
- console.log(`📦 Using: ${juxscriptPath}`);
489
-
490
- await this.loadJuxscriptExports();
491
172
 
492
- if (fs.existsSync(this.distDir)) {
493
- fs.rmSync(this.distDir, { recursive: true, force: true });
494
- }
495
- fs.mkdirSync(this.distDir, { recursive: true });
173
+ console.log('📦 Copying public assets...');
496
174
 
497
- // Copy public folder if exists
498
- this.copyPublicFolder();
175
+ const copyRecursive = (src, dest) => {
176
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
499
177
 
500
- const { views, dataModules, sharedModules } = this.scanFiles();
501
- console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
178
+ const entries = fs.readdirSync(src, { withFileTypes: true });
502
179
 
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
- });
180
+ for (const entry of entries) {
181
+ const srcPath = path.join(src, entry.name);
182
+ const destPath = path.join(dest, entry.name);
509
183
 
510
- const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
511
- const entryPath = path.join(this.distDir, 'entry.js');
512
- fs.writeFileSync(entryPath, entryContent);
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
+ };
513
195
 
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`);
196
+ copyRecursive(publicPath, this.distDir);
197
+ console.log('✅ Public assets copied');
198
+ }
517
199
 
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
- }
200
+ /* ═══════════════════════════════════════════════════════════════════
201
+ * GENERATE index.html
202
+ * ═══════════════════════════════════════════════════════════════════ */
523
203
 
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
- }
204
+ async generateIndexHtml() {
205
+ console.log(' ✅ Generated index.html');
546
206
 
547
207
  const html = `<!DOCTYPE html>
548
208
  <html lang="en">
@@ -550,115 +210,80 @@ document.addEventListener('click', (e) => {
550
210
  <meta charset="UTF-8">
551
211
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
552
212
  <title>JUX Application</title>
553
- <script type="module" src="./bundle.js"></script>
554
- <script src="./entry.js"></script>
213
+ <link rel="stylesheet" href="/styles.css">
214
+ <script src="https://unpkg.com/lucide@latest"></script>
555
215
  </head>
556
216
  <body>
557
217
  <div id="app"></div>
218
+ <script src="/entry.js"></script>
558
219
  </body>
559
220
  </html>`;
560
221
 
561
- fs.writeFileSync(
562
- path.join(this.config.distDir, 'index.html'),
563
- html,
564
- 'utf8'
565
- );
566
-
567
- console.log(' ✅ Generated index.html');
568
- return { success: true, errors: [], warnings: validation.warnings };
222
+ fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
569
223
  }
570
224
 
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
- }
583
-
584
- console.log('📦 Copying public assets...');
225
+ /* ═══════════════════════════════════════════════════════════════════
226
+ * BUILD
227
+ * ═══════════════════════════════════════════════════════════════════ */
585
228
 
229
+ async build() {
586
230
  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
- }
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 });
593
236
 
594
- /**
595
- * Recursively copy directory contents
596
- */
597
- _copyDirRecursive(src, dest, depth = 0) {
598
- const entries = fs.readdirSync(src, { withFileTypes: true });
237
+ // Copy public assets first
238
+ await this.copyPublicAssets();
599
239
 
600
- entries.forEach(entry => {
601
- // Skip hidden files and directories
602
- if (entry.name.startsWith('.')) return;
240
+ // Scan files
241
+ const { views, dataModules, sharedModules } = this.scanFiles();
242
+ console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
603
243
 
604
- const srcPath = path.join(src, entry.name);
605
- const destPath = path.join(dest, entry.name);
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');
606
248
 
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
- }
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
+ });
626
260
 
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
- }
261
+ console.log('✅ esbuild complete');
262
+
263
+ // Clean up temp file
264
+ fs.unlinkSync(entryPath);
649
265
 
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
266
+ // Generate index.html
267
+ await this.generateIndexHtml();
268
+
269
+ // Return success object
270
+ return {
271
+ success: true,
272
+ errors: [],
273
+ warnings: [],
274
+ fileCount: views.length
275
+ };
276
+
277
+ } catch (error) {
278
+ console.error('\n❌ Build failed:', error.message);
279
+ console.error(error.stack);
280
+
281
+ // ✅ Return failure object
282
+ return {
283
+ success: false,
284
+ errors: [error.message],
285
+ warnings: []
286
+ };
287
+ }
663
288
  }
664
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.127",
3
+ "version": "1.1.129",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",