juxscript 1.1.128 → 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:38:34.158Z",
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,224 +116,162 @@ 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
160
 
460
- // Initial route
461
- route(window.location.pathname);
161
+ /* ═══════════════════════════════════════════════════════════════════
162
+ * COPY PUBLIC ASSETS
163
+ * ═══════════════════════════════════════════════════════════════════ */
462
164
 
463
- // Handle navigation
464
- window.addEventListener('popstate', () => route(window.location.pathname));
165
+ async copyPublicAssets() {
166
+ const publicPath = this.paths?.public || path.resolve(this.srcDir, '..', this.publicDir);
465
167
 
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);
168
+ if (!fs.existsSync(publicPath)) {
169
+ console.log(`ℹ️ No public folder found, skipping`);
170
+ return;
474
171
  }
172
+
173
+ console.log('📦 Copying public assets...');
174
+
175
+ const copyRecursive = (src, dest) => {
176
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
177
+
178
+ const entries = fs.readdirSync(src, { withFileTypes: true });
179
+
180
+ for (const entry of entries) {
181
+ const srcPath = path.join(src, entry.name);
182
+ const destPath = path.join(dest, entry.name);
183
+
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
+ };
195
+
196
+ copyRecursive(publicPath, this.distDir);
197
+ console.log('✅ Public assets copied');
475
198
  }
476
- });
477
- `;
199
+
200
+ /* ═══════════════════════════════════════════════════════════════════
201
+ * GENERATE index.html
202
+ * ═══════════════════════════════════════════════════════════════════ */
203
+
204
+ async generateIndexHtml() {
205
+ console.log(' ✅ Generated index.html');
206
+
207
+ const html = `<!DOCTYPE html>
208
+ <html lang="en">
209
+ <head>
210
+ <meta charset="UTF-8">
211
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
212
+ <title>JUX Application</title>
213
+ <link rel="stylesheet" href="/styles.css">
214
+ <script src="https://unpkg.com/lucide@latest"></script>
215
+ </head>
216
+ <body>
217
+ <div id="app"></div>
218
+ <script src="/entry.js"></script>
219
+ </body>
220
+ </html>`;
221
+
222
+ fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
478
223
  }
479
224
 
480
- async build() {
481
- console.log('🔨 Building JUX project...\n');
225
+ /* ═══════════════════════════════════════════════════════════════════
226
+ * BUILD
227
+ * ═══════════════════════════════════════════════════════════════════ */
482
228
 
229
+ async build() {
483
230
  try {
484
- // 1. Clean dist
485
- if (fs.existsSync(this.config.distDir)) {
486
- fs.rmSync(this.config.distDir, { recursive: true, force: true });
231
+ // Clean dist
232
+ if (fs.existsSync(this.distDir)) {
233
+ fs.rmSync(this.distDir, { recursive: true, force: true });
487
234
  }
488
- fs.mkdirSync(this.config.distDir, { recursive: true });
235
+ fs.mkdirSync(this.distDir, { recursive: true });
489
236
 
490
- // 2. Scan .jux files recursively
491
- const juxFiles = this.scanJuxFiles(this.config.srcDir);
492
- console.log(`📂 Found ${juxFiles.length} .jux files\n`);
237
+ // Copy public assets first
238
+ await this.copyPublicAssets();
493
239
 
494
- // 3. ✅ Bundle all .jux files → entry.js
495
- await this.bundleJuxFiles(juxFiles);
240
+ // Scan files
241
+ const { views, dataModules, sharedModules } = this.scanFiles();
242
+ console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
243
+
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');
248
+
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
+ });
496
260
 
497
- // 4. Bundle vendor libraries → bundle.js
498
- await this.bundleVendorFiles();
261
+ console.log('esbuild complete');
499
262
 
500
- // 5. Copy public assets (CSS, images, etc.)
501
- await this.copyPublicAssets();
263
+ // Clean up temp file
264
+ fs.unlinkSync(entryPath);
502
265
 
503
- // 6. Generate index.html (with entry.js + bundle.js)
266
+ // Generate index.html
504
267
  await this.generateIndexHtml();
505
268
 
506
- console.log('\nBuild completed successfully!\n');
507
- console.log(`📁 Output: ${this.config.distDir}`);
508
- console.log(` ✓ entry.js (${juxFiles.length} .jux files bundled)`);
509
- console.log(` ✓ bundle.js (vendor libraries)`);
510
- console.log(` ✓ index.html`);
511
- console.log(` ✓ Public assets copied\n`);
512
-
513
- // ✅ Consistent return object
269
+ // Return success object
514
270
  return {
515
271
  success: true,
516
272
  errors: [],
517
273
  warnings: [],
518
- fileCount: juxFiles.length
274
+ fileCount: views.length
519
275
  };
520
276
 
521
277
  } catch (error) {
@@ -530,98 +286,4 @@ document.addEventListener('click', (e) => {
530
286
  };
531
287
  }
532
288
  }
533
-
534
- /**
535
- * Copy public folder contents to dist
536
- */
537
- copyPublicFolder() {
538
- // ✅ Use configured public path or resolve from paths object
539
- const publicSrc = this.paths.public
540
- ? this.paths.public
541
- : path.resolve(process.cwd(), this.publicDir);
542
-
543
- if (!fs.existsSync(publicSrc)) {
544
- return; // No public folder, skip
545
- }
546
-
547
- console.log('📦 Copying public assets...');
548
-
549
- try {
550
- this._copyDirRecursive(publicSrc, this.distDir, 0);
551
- console.log('✅ Public assets copied');
552
- } catch (err) {
553
- console.warn('⚠️ Error copying public folder:', err.message);
554
- }
555
- }
556
-
557
- /**
558
- * Recursively copy directory contents
559
- */
560
- _copyDirRecursive(src, dest, depth = 0) {
561
- const entries = fs.readdirSync(src, { withFileTypes: true });
562
-
563
- entries.forEach(entry => {
564
- // Skip hidden files and directories
565
- if (entry.name.startsWith('.')) return;
566
-
567
- const srcPath = path.join(src, entry.name);
568
- const destPath = path.join(dest, entry.name);
569
-
570
- if (entry.isDirectory()) {
571
- // Create directory and recurse
572
- if (!fs.existsSync(destPath)) {
573
- fs.mkdirSync(destPath, { recursive: true });
574
- }
575
- this._copyDirRecursive(srcPath, destPath, depth + 1);
576
- } else {
577
- // Copy file
578
- fs.copyFileSync(srcPath, destPath);
579
-
580
- // Log files at root level only
581
- if (depth === 0) {
582
- const ext = path.extname(entry.name);
583
- const icon = this._getFileIcon(ext);
584
- console.log(` ${icon} ${entry.name}`);
585
- }
586
- }
587
- });
588
- }
589
-
590
- /**
591
- * Get icon for file type
592
- */
593
- _getFileIcon(ext) {
594
- const icons = {
595
- '.html': '📄',
596
- '.css': '🎨',
597
- '.js': '📜',
598
- '.json': '📋',
599
- '.png': '🖼️',
600
- '.jpg': '🖼️',
601
- '.jpeg': '🖼️',
602
- '.gif': '🖼️',
603
- '.svg': '🎨',
604
- '.ico': '🔖',
605
- '.woff': '🔤',
606
- '.woff2': '🔤',
607
- '.ttf': '🔤',
608
- '.eot': '🔤'
609
- };
610
- return icons[ext.toLowerCase()] || '📦';
611
- }
612
-
613
- /**
614
- * ✅ Generate valid JavaScript identifier from file path
615
- * Example: abc/aaa.jux -> abc_aaa
616
- * Example: menus/main.jux -> menus_main
617
- * Example: pages/blog/post.jux -> pages_blog_post
618
- */
619
- _generateNameFromPath(filepath) {
620
- return filepath
621
- .replace(/\.jux$/, '') // Remove .jux extension
622
- .replace(/[\/\\]/g, '_') // Replace / and \ with _
623
- .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
624
- .replace(/_+/g, '_') // Collapse multiple consecutive underscores
625
- .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
626
- }
627
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.128",
3
+ "version": "1.1.129",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",