juxscript 1.1.117 → 1.1.118
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.
- package/dom-structure-map.json +1 -1
- package/machinery/compiler3.js +98 -585
- package/package.json +1 -1
package/dom-structure-map.json
CHANGED
package/machinery/compiler3.js
CHANGED
|
@@ -203,160 +203,20 @@ export class JuxCompiler {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
* ✅ Generate
|
|
207
|
-
* Examples:
|
|
208
|
-
* index.jux -> /
|
|
209
|
-
* abc/juxabc.jux -> /abc/juxabc
|
|
210
|
-
* abc/index.jux -> /abc
|
|
211
|
-
*/
|
|
212
|
-
_generateRoutePath(filePath) {
|
|
213
|
-
// Normalize separators
|
|
214
|
-
const normalized = filePath.replace(/\\/g, '/');
|
|
215
|
-
|
|
216
|
-
// Remove extension
|
|
217
|
-
const withoutExt = normalized.replace(/\.[^/.]+$/, '');
|
|
218
|
-
|
|
219
|
-
// Handle index files
|
|
220
|
-
if (withoutExt === 'index') {
|
|
221
|
-
return '/';
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Remove trailing /index
|
|
225
|
-
const cleaned = withoutExt.replace(/\/index$/, '');
|
|
226
|
-
|
|
227
|
-
// Ensure leading slash
|
|
228
|
-
return '/' + cleaned.toLowerCase();
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* ✅ Generate PascalCase function name from sanitized name
|
|
233
|
-
* Examples:
|
|
234
|
-
* abc_juxabc -> AbcJuxabc
|
|
235
|
-
* index -> Index
|
|
236
|
-
*/
|
|
237
|
-
_generateFunctionName(name) {
|
|
238
|
-
return name
|
|
239
|
-
.split('_')
|
|
240
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
241
|
-
.join('');
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/**
|
|
245
|
-
* ✅ Generate entry point with nested folder support
|
|
246
|
-
*/
|
|
247
|
-
generateEntryPoint(views, dataModules, sharedModules) {
|
|
248
|
-
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
249
|
-
const allIssues = [];
|
|
250
|
-
const sourceSnapshot = {};
|
|
251
|
-
|
|
252
|
-
const juxImports = new Set();
|
|
253
|
-
const layoutImports = new Set(); // ✅ Track layout imports separately
|
|
254
|
-
|
|
255
|
-
// Scan for imports
|
|
256
|
-
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
257
|
-
// Regular juxscript imports
|
|
258
|
-
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
259
|
-
match[1].split(',').map(s => s.trim()).forEach(imp => {
|
|
260
|
-
if (imp) {
|
|
261
|
-
// ✅ Separate layout imports
|
|
262
|
-
if (imp === 'VStack' || imp === 'HStack' || imp === 'ZStack') {
|
|
263
|
-
layoutImports.add(imp);
|
|
264
|
-
} else {
|
|
265
|
-
juxImports.add(imp);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
// ✅ Import layouts separately
|
|
273
|
-
if (layoutImports.size > 0) {
|
|
274
|
-
entry += `import { ${[...layoutImports].sort().join(', ')} } from 'juxscript';\n`;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// ✅ Import regular components
|
|
278
|
-
if (juxImports.size > 0) {
|
|
279
|
-
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Data and shared modules
|
|
283
|
-
dataModules.forEach(m => {
|
|
284
|
-
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
285
|
-
});
|
|
286
|
-
sharedModules.forEach(m => {
|
|
287
|
-
entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
entry += `\n// Expose to window\n`;
|
|
291
|
-
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
292
|
-
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
293
|
-
|
|
294
|
-
// ✅ Expose layouts to window
|
|
295
|
-
if (layoutImports.size > 0) {
|
|
296
|
-
entry += `\n// Expose layout components\n`;
|
|
297
|
-
layoutImports.forEach(layout => {
|
|
298
|
-
entry += `window.${layout} = ${layout};\n`;
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ✅ Expose regular components
|
|
303
|
-
if (juxImports.size > 0) {
|
|
304
|
-
entry += `\n// Expose components\n`;
|
|
305
|
-
entry += `Object.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
309
|
-
|
|
310
|
-
views.forEach(v => {
|
|
311
|
-
const functionName = this._generateFunctionName(v.name);
|
|
312
|
-
allIssues.push(...this.validateViewCode(v.name, v.content));
|
|
313
|
-
|
|
314
|
-
sourceSnapshot[v.file] = {
|
|
315
|
-
name: v.name,
|
|
316
|
-
file: v.file,
|
|
317
|
-
content: v.content,
|
|
318
|
-
lines: v.content.split('\n')
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
322
|
-
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
323
|
-
|
|
324
|
-
entry += `\n${asyncPrefix}function render${functionName}() {\n${viewCode}\n}\n`;
|
|
325
|
-
});
|
|
326
|
-
|
|
327
|
-
dataModules.forEach(m => {
|
|
328
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
329
|
-
});
|
|
330
|
-
sharedModules.forEach(m => {
|
|
331
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
this._sourceSnapshot = sourceSnapshot;
|
|
335
|
-
this._validationIssues = allIssues;
|
|
336
|
-
entry += this._generateRouter(views);
|
|
337
|
-
return entry;
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
reportValidationIssues() {
|
|
341
|
-
if (!this._validationIssues || this._validationIssues.length === 0) return;
|
|
342
|
-
|
|
343
|
-
console.log('\n⚠️ Validation Issues:\n');
|
|
344
|
-
this._validationIssues.forEach(issue => {
|
|
345
|
-
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
346
|
-
console.log(` ${icon} ${issue.view}:${issue.line} - ${issue.message}`);
|
|
347
|
-
});
|
|
348
|
-
console.log('');
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/**
|
|
352
|
-
* ✅ Generate routes based on folder structure (SINGLE DEFINITION)
|
|
206
|
+
* ✅ Generate routes based on folder structure
|
|
353
207
|
*/
|
|
354
208
|
_generateRouter(views) {
|
|
355
209
|
let routeMap = '';
|
|
356
210
|
|
|
357
211
|
views.forEach(v => {
|
|
212
|
+
// ✅ Generate route from folder structure
|
|
213
|
+
// abc/juxabc.jux -> /abc/juxabc
|
|
214
|
+
// index.jux -> /
|
|
215
|
+
// abc/index.jux -> /abc
|
|
216
|
+
|
|
358
217
|
const routePath = this._generateRoutePath(v.file);
|
|
359
218
|
const functionName = this._generateFunctionName(v.name);
|
|
219
|
+
|
|
360
220
|
routeMap += ` '${routePath}': render${functionName},\n`;
|
|
361
221
|
});
|
|
362
222
|
|
|
@@ -488,8 +348,7 @@ window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error ||
|
|
|
488
348
|
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
489
349
|
|
|
490
350
|
// --- JUX ROUTER ---
|
|
491
|
-
const routes = {
|
|
492
|
-
${routeMap}};
|
|
351
|
+
const routes = {\n${routeMap}};
|
|
493
352
|
|
|
494
353
|
async function navigate(path) {
|
|
495
354
|
const view = routes[path];
|
|
@@ -518,218 +377,29 @@ navigate(location.pathname);
|
|
|
518
377
|
}
|
|
519
378
|
|
|
520
379
|
/**
|
|
521
|
-
* ✅
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
// Ensure dist/jux directory exists
|
|
527
|
-
if (!fs.existsSync(distJuxDir)) {
|
|
528
|
-
fs.mkdirSync(distJuxDir, { recursive: true });
|
|
529
|
-
}
|
|
530
|
-
|
|
531
|
-
// Copy all .jux and .js files from source to dist
|
|
532
|
-
this._copySourceFilesRecursive(this.srcDir, this.srcDir, distJuxDir);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Recursively copy .jux and .js files preserving folder structure
|
|
537
|
-
*/
|
|
538
|
-
_copySourceFilesRecursive(src, baseDir, distBase) {
|
|
539
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
540
|
-
|
|
541
|
-
entries.forEach(entry => {
|
|
542
|
-
if (entry.name.startsWith('.')) return; // Skip hidden
|
|
543
|
-
|
|
544
|
-
const srcPath = path.join(src, entry.name);
|
|
545
|
-
const relativePath = path.relative(baseDir, srcPath);
|
|
546
|
-
const destPath = path.join(distBase, relativePath);
|
|
547
|
-
|
|
548
|
-
if (entry.isDirectory()) {
|
|
549
|
-
// Create directory if it doesn't exist
|
|
550
|
-
if (!fs.existsSync(destPath)) {
|
|
551
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
552
|
-
}
|
|
553
|
-
// Recurse into subdirectory
|
|
554
|
-
this._copySourceFilesRecursive(srcPath, baseDir, distBase);
|
|
555
|
-
} else if (entry.name.endsWith('.jux') || entry.name.endsWith('.js')) {
|
|
556
|
-
// Copy .jux and .js files (skip assets)
|
|
557
|
-
if (!this.isAssetFile(entry.name)) {
|
|
558
|
-
// Ensure parent directory exists
|
|
559
|
-
const destDir = path.dirname(destPath);
|
|
560
|
-
if (!fs.existsSync(destDir)) {
|
|
561
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
562
|
-
}
|
|
563
|
-
fs.copyFileSync(srcPath, destPath);
|
|
564
|
-
console.log(` 📋 Copied: ${relativePath}`);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
});
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Copy ONLY public assets (CSS, images, fonts) to dist
|
|
572
|
-
* ❌ Does NOT copy .jux files (they are compiled separately)
|
|
573
|
-
*/
|
|
574
|
-
async copyPublicAssets() {
|
|
575
|
-
const { publicDir, distDir, paths } = this.config;
|
|
576
|
-
|
|
577
|
-
// Resolve public folder path
|
|
578
|
-
const publicPath = paths?.public || path.resolve(this.config.srcDir, '..', publicDir);
|
|
579
|
-
|
|
580
|
-
if (!fs.existsSync(publicPath)) {
|
|
581
|
-
console.log(`ℹ️ No public folder found at ${publicPath}, skipping asset copy`);
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
console.log(`📦 Copying public assets from ${publicPath}...`);
|
|
586
|
-
|
|
587
|
-
// ✅ ONLY copy known asset file types
|
|
588
|
-
const assetTypes = [
|
|
589
|
-
'.css', '.scss', '.sass', '.less', // Stylesheets
|
|
590
|
-
'.jpg', '.jpeg', '.png', '.gif', '.svg', // Images
|
|
591
|
-
'.ico', '.webp', '.avif', // Icons/modern images
|
|
592
|
-
'.woff', '.woff2', '.ttf', '.eot', '.otf', // Fonts
|
|
593
|
-
'.mp4', '.webm', '.ogg', // Video
|
|
594
|
-
'.mp3', '.wav', '.m4a', // Audio
|
|
595
|
-
'.pdf', '.txt', '.json', '.xml' // Documents
|
|
596
|
-
];
|
|
597
|
-
|
|
598
|
-
const copyRecursive = (src, dest) => {
|
|
599
|
-
if (!fs.existsSync(src)) return;
|
|
600
|
-
|
|
601
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
602
|
-
|
|
603
|
-
for (const entry of entries) {
|
|
604
|
-
const srcPath = path.join(src, entry.name);
|
|
605
|
-
const destPath = path.join(dest, entry.name);
|
|
606
|
-
|
|
607
|
-
if (entry.isDirectory()) {
|
|
608
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
609
|
-
copyRecursive(srcPath, destPath);
|
|
610
|
-
} else {
|
|
611
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
612
|
-
|
|
613
|
-
// ✅ ONLY copy known asset types
|
|
614
|
-
// ❌ SKIP .jux files (they're bundled into entry.js)
|
|
615
|
-
if (assetTypes.includes(ext)) {
|
|
616
|
-
fs.copyFileSync(srcPath, destPath);
|
|
617
|
-
console.log(` ✓ ${entry.name}`);
|
|
618
|
-
} else if (ext === '.jux') {
|
|
619
|
-
// Silently skip .jux files
|
|
620
|
-
continue;
|
|
621
|
-
} else {
|
|
622
|
-
// Warn about unknown file types
|
|
623
|
-
console.warn(` ⚠️ Skipped: ${entry.name} (unknown type: ${ext})`);
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
copyRecursive(publicPath, distDir);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
/**
|
|
633
|
-
* Main build pipeline
|
|
380
|
+
* ✅ Generate route path from file path
|
|
381
|
+
* Examples:
|
|
382
|
+
* index.jux -> /
|
|
383
|
+
* abc/juxabc.jux -> /abc/juxabc
|
|
384
|
+
* abc/index.jux -> /abc
|
|
634
385
|
*/
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
try {
|
|
639
|
-
// 1. Clean dist
|
|
640
|
-
if (fs.existsSync(this.config.distDir)) {
|
|
641
|
-
fs.rmSync(this.config.distDir, { recursive: true, force: true });
|
|
642
|
-
}
|
|
643
|
-
fs.mkdirSync(this.config.distDir, { recursive: true });
|
|
644
|
-
|
|
645
|
-
// 2. Scan .jux files
|
|
646
|
-
const juxFiles = this.scanJuxFiles(this.config.srcDir);
|
|
647
|
-
console.log(`📂 Found ${juxFiles.length} .jux files\n`);
|
|
648
|
-
|
|
649
|
-
// 3. ✅ Bundle all .jux files → entry.js
|
|
650
|
-
await this.bundleJuxFiles(juxFiles);
|
|
651
|
-
|
|
652
|
-
// 4. ✅ Bundle vendor libraries → bundle.js
|
|
653
|
-
await this.bundleVendorFiles();
|
|
654
|
-
|
|
655
|
-
// 5. Copy public assets (CSS, images, etc.)
|
|
656
|
-
await this.copyPublicAssets();
|
|
657
|
-
|
|
658
|
-
// 6. Generate index.html (with entry.js + bundle.js)
|
|
659
|
-
await this.generateIndexHtml();
|
|
660
|
-
|
|
661
|
-
console.log('\n✅ Build completed successfully!\n');
|
|
662
|
-
console.log(`📁 Output: ${this.config.distDir}`);
|
|
663
|
-
console.log(` ✓ entry.js (${juxFiles.length} .jux files bundled)`);
|
|
664
|
-
console.log(` ✓ bundle.js (vendor libraries)`);
|
|
665
|
-
console.log(` ✓ index.html`);
|
|
666
|
-
console.log(` ✓ Public assets copied\n`);
|
|
386
|
+
_generateRoutePath(filePath) {
|
|
387
|
+
// Normalize separators
|
|
388
|
+
const normalized = filePath.replace(/\\/g, '/');
|
|
667
389
|
|
|
668
|
-
|
|
390
|
+
// Remove extension
|
|
391
|
+
const withoutExt = normalized.replace(/\.[^/.]+$/, '');
|
|
669
392
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
return { success: false, error };
|
|
393
|
+
// Handle index files
|
|
394
|
+
if (withoutExt === 'index') {
|
|
395
|
+
return '/';
|
|
674
396
|
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* Scan for .jux files ONLY in source directory
|
|
679
|
-
*/
|
|
680
|
-
scanJuxFiles(dir) {
|
|
681
|
-
const juxFiles = [];
|
|
682
|
-
|
|
683
|
-
const scan = (currentDir) => {
|
|
684
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
685
|
-
|
|
686
|
-
for (const entry of entries) {
|
|
687
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
688
|
-
|
|
689
|
-
if (entry.isDirectory()) {
|
|
690
|
-
scan(fullPath);
|
|
691
|
-
} else if (entry.name.endsWith('.jux')) {
|
|
692
|
-
juxFiles.push(fullPath);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
};
|
|
696
|
-
|
|
697
|
-
scan(dir);
|
|
698
|
-
return juxFiles;
|
|
699
|
-
}
|
|
700
397
|
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
*/
|
|
704
|
-
async compileFile(juxFilePath) {
|
|
705
|
-
const relativePath = path.relative(this.config.srcDir, juxFilePath);
|
|
706
|
-
const outputPath = path.join(this.config.distDir, relativePath.replace(/\.jux$/, '.js'));
|
|
707
|
-
|
|
708
|
-
// Ensure output directory exists
|
|
709
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
710
|
-
|
|
711
|
-
// Read .jux source
|
|
712
|
-
const juxCode = fs.readFileSync(juxFilePath, 'utf8');
|
|
713
|
-
|
|
714
|
-
// Compile to JavaScript
|
|
715
|
-
const jsCode = this.transformJuxToJs(juxCode);
|
|
716
|
-
|
|
717
|
-
// Write compiled .js file
|
|
718
|
-
fs.writeFileSync(outputPath, jsCode, 'utf8');
|
|
719
|
-
|
|
720
|
-
console.log(` ✓ ${relativePath} → ${path.basename(outputPath)}`);
|
|
721
|
-
}
|
|
398
|
+
// Remove trailing /index
|
|
399
|
+
const cleaned = withoutExt.replace(/\/index$/, '');
|
|
722
400
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
* Example: abc/juxabc.jux -> abc_juxabc
|
|
726
|
-
*/
|
|
727
|
-
_generateNameFromPath(path) {
|
|
728
|
-
return path
|
|
729
|
-
.replace(/\.[^/.]+$/, '') // Remove extension
|
|
730
|
-
.replace(/\\/g, '/') // Normalize Windows paths
|
|
731
|
-
.replace(/\//g, '_') // Folder separator -> underscore
|
|
732
|
-
.replace(/[^a-zA-Z0-9_]/g, '_'); // Sanitize
|
|
401
|
+
// Ensure leading slash
|
|
402
|
+
return '/' + cleaned.toLowerCase();
|
|
733
403
|
}
|
|
734
404
|
|
|
735
405
|
/**
|
|
@@ -754,36 +424,18 @@ navigate(location.pathname);
|
|
|
754
424
|
const sourceSnapshot = {};
|
|
755
425
|
|
|
756
426
|
const juxImports = new Set();
|
|
757
|
-
const layoutImports = new Set(); // ✅ Track layout imports separately
|
|
758
|
-
|
|
759
|
-
// Scan for imports
|
|
760
427
|
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
761
|
-
// Regular juxscript imports
|
|
762
428
|
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
763
429
|
match[1].split(',').map(s => s.trim()).forEach(imp => {
|
|
764
|
-
if (imp)
|
|
765
|
-
// ✅ Separate layout imports
|
|
766
|
-
if (imp === 'VStack' || imp === 'HStack' || imp === 'ZStack') {
|
|
767
|
-
layoutImports.add(imp);
|
|
768
|
-
} else {
|
|
769
|
-
juxImports.add(imp);
|
|
770
|
-
}
|
|
771
|
-
}
|
|
430
|
+
if (imp) juxImports.add(imp);
|
|
772
431
|
});
|
|
773
432
|
}
|
|
774
433
|
});
|
|
775
434
|
|
|
776
|
-
// ✅ Import layouts separately
|
|
777
|
-
if (layoutImports.size > 0) {
|
|
778
|
-
entry += `import { ${[...layoutImports].sort().join(', ')} } from 'juxscript';\n`;
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// ✅ Import regular components
|
|
782
435
|
if (juxImports.size > 0) {
|
|
783
436
|
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
784
437
|
}
|
|
785
438
|
|
|
786
|
-
// Data and shared modules
|
|
787
439
|
dataModules.forEach(m => {
|
|
788
440
|
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
789
441
|
});
|
|
@@ -795,18 +447,8 @@ navigate(location.pathname);
|
|
|
795
447
|
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
796
448
|
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
797
449
|
|
|
798
|
-
// ✅ Expose layouts to window
|
|
799
|
-
if (layoutImports.size > 0) {
|
|
800
|
-
entry += `\n// Expose layout components\n`;
|
|
801
|
-
layoutImports.forEach(layout => {
|
|
802
|
-
entry += `window.${layout} = ${layout};\n`;
|
|
803
|
-
});
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
// ✅ Expose regular components
|
|
807
450
|
if (juxImports.size > 0) {
|
|
808
|
-
entry += `\
|
|
809
|
-
entry += `Object.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
451
|
+
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
810
452
|
}
|
|
811
453
|
|
|
812
454
|
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
@@ -842,25 +484,34 @@ navigate(location.pathname);
|
|
|
842
484
|
}
|
|
843
485
|
|
|
844
486
|
reportValidationIssues() {
|
|
845
|
-
|
|
487
|
+
const issues = this._validationIssues || [];
|
|
488
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
489
|
+
const warnings = issues.filter(i => i.type === 'warning');
|
|
490
|
+
|
|
491
|
+
if (issues.length > 0) {
|
|
492
|
+
console.log('\n⚠️ Validation Issues:\n');
|
|
493
|
+
issues.forEach(issue => {
|
|
494
|
+
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
495
|
+
console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
|
|
496
|
+
});
|
|
497
|
+
console.log('');
|
|
498
|
+
}
|
|
846
499
|
|
|
847
|
-
|
|
848
|
-
this._validationIssues.forEach(issue => {
|
|
849
|
-
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
850
|
-
console.log(` ${icon} ${issue.view}:${issue.line} - ${issue.message}`);
|
|
851
|
-
});
|
|
852
|
-
console.log('');
|
|
500
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
853
501
|
}
|
|
854
502
|
|
|
855
|
-
/**
|
|
856
|
-
* ✅ Generate routes based on folder structure (SINGLE DEFINITION)
|
|
857
|
-
*/
|
|
858
503
|
_generateRouter(views) {
|
|
859
504
|
let routeMap = '';
|
|
860
505
|
|
|
861
506
|
views.forEach(v => {
|
|
507
|
+
// ✅ Generate route from folder structure
|
|
508
|
+
// abc/juxabc.jux -> /abc/juxabc
|
|
509
|
+
// index.jux -> /
|
|
510
|
+
// abc/index.jux -> /abc
|
|
511
|
+
|
|
862
512
|
const routePath = this._generateRoutePath(v.file);
|
|
863
513
|
const functionName = this._generateFunctionName(v.name);
|
|
514
|
+
|
|
864
515
|
routeMap += ` '${routePath}': render${functionName},\n`;
|
|
865
516
|
});
|
|
866
517
|
|
|
@@ -992,8 +643,7 @@ window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error ||
|
|
|
992
643
|
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
993
644
|
|
|
994
645
|
// --- JUX ROUTER ---
|
|
995
|
-
const routes = {
|
|
996
|
-
${routeMap}};
|
|
646
|
+
const routes = {\n${routeMap}};
|
|
997
647
|
|
|
998
648
|
async function navigate(path) {
|
|
999
649
|
const view = routes[path];
|
|
@@ -1022,218 +672,81 @@ navigate(location.pathname);
|
|
|
1022
672
|
}
|
|
1023
673
|
|
|
1024
674
|
/**
|
|
1025
|
-
*
|
|
675
|
+
* Copy public folder contents to dist
|
|
1026
676
|
*/
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
677
|
+
copyPublicFolder() {
|
|
678
|
+
// ✅ Use configured public path or resolve from paths object
|
|
679
|
+
const publicSrc = this.paths.public
|
|
680
|
+
? this.paths.public
|
|
681
|
+
: path.resolve(process.cwd(), this.publicDir);
|
|
682
|
+
|
|
683
|
+
if (!fs.existsSync(publicSrc)) {
|
|
684
|
+
return; // No public folder, skip
|
|
1033
685
|
}
|
|
1034
686
|
|
|
1035
|
-
|
|
1036
|
-
|
|
687
|
+
console.log('📦 Copying public assets...');
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
this._copyDirRecursive(publicSrc, this.distDir, 0);
|
|
691
|
+
console.log('✅ Public assets copied');
|
|
692
|
+
} catch (err) {
|
|
693
|
+
console.warn('⚠️ Error copying public folder:', err.message);
|
|
694
|
+
}
|
|
1037
695
|
}
|
|
1038
696
|
|
|
1039
697
|
/**
|
|
1040
|
-
* Recursively copy
|
|
698
|
+
* Recursively copy directory contents
|
|
1041
699
|
*/
|
|
1042
|
-
|
|
700
|
+
_copyDirRecursive(src, dest, depth = 0) {
|
|
1043
701
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1044
702
|
|
|
1045
703
|
entries.forEach(entry => {
|
|
1046
|
-
|
|
704
|
+
// Skip hidden files and directories
|
|
705
|
+
if (entry.name.startsWith('.')) return;
|
|
1047
706
|
|
|
1048
707
|
const srcPath = path.join(src, entry.name);
|
|
1049
|
-
const
|
|
1050
|
-
const destPath = path.join(distBase, relativePath);
|
|
708
|
+
const destPath = path.join(dest, entry.name);
|
|
1051
709
|
|
|
1052
710
|
if (entry.isDirectory()) {
|
|
1053
|
-
// Create directory
|
|
711
|
+
// Create directory and recurse
|
|
1054
712
|
if (!fs.existsSync(destPath)) {
|
|
1055
713
|
fs.mkdirSync(destPath, { recursive: true });
|
|
1056
714
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1068
|
-
console.log(` 📋 Copied: ${relativePath}`);
|
|
715
|
+
this._copyDirRecursive(srcPath, destPath, depth + 1);
|
|
716
|
+
} else {
|
|
717
|
+
// Copy file
|
|
718
|
+
fs.copyFileSync(srcPath, destPath);
|
|
719
|
+
|
|
720
|
+
// Log files at root level only
|
|
721
|
+
if (depth === 0) {
|
|
722
|
+
const ext = path.extname(entry.name);
|
|
723
|
+
const icon = this._getFileIcon(ext);
|
|
724
|
+
console.log(` ${icon} ${entry.name}`);
|
|
1069
725
|
}
|
|
1070
726
|
}
|
|
1071
727
|
});
|
|
1072
728
|
}
|
|
1073
729
|
|
|
1074
730
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
* ❌ Does NOT copy .jux files (they are compiled separately)
|
|
731
|
+
* Get icon for file type
|
|
1077
732
|
*/
|
|
1078
|
-
|
|
1079
|
-
const
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
'.
|
|
1094
|
-
'.jpg', '.jpeg', '.png', '.gif', '.svg', // Images
|
|
1095
|
-
'.ico', '.webp', '.avif', // Icons/modern images
|
|
1096
|
-
'.woff', '.woff2', '.ttf', '.eot', '.otf', // Fonts
|
|
1097
|
-
'.mp4', '.webm', '.ogg', // Video
|
|
1098
|
-
'.mp3', '.wav', '.m4a', // Audio
|
|
1099
|
-
'.pdf', '.txt', '.json', '.xml' // Documents
|
|
1100
|
-
];
|
|
1101
|
-
|
|
1102
|
-
const copyRecursive = (src, dest) => {
|
|
1103
|
-
if (!fs.existsSync(src)) return;
|
|
1104
|
-
|
|
1105
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1106
|
-
|
|
1107
|
-
for (const entry of entries) {
|
|
1108
|
-
const srcPath = path.join(src, entry.name);
|
|
1109
|
-
const destPath = path.join(dest, entry.name);
|
|
1110
|
-
|
|
1111
|
-
if (entry.isDirectory()) {
|
|
1112
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
1113
|
-
copyRecursive(srcPath, destPath);
|
|
1114
|
-
} else {
|
|
1115
|
-
const ext = path.extname(entry.name).toLowerCase();
|
|
1116
|
-
|
|
1117
|
-
// ✅ ONLY copy known asset types
|
|
1118
|
-
// ❌ SKIP .jux files (they're bundled into entry.js)
|
|
1119
|
-
if (assetTypes.includes(ext)) {
|
|
1120
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1121
|
-
console.log(` ✓ ${entry.name}`);
|
|
1122
|
-
} else if (ext === '.jux') {
|
|
1123
|
-
// Silently skip .jux files
|
|
1124
|
-
continue;
|
|
1125
|
-
} else {
|
|
1126
|
-
// Warn about unknown file types
|
|
1127
|
-
console.warn(` ⚠️ Skipped: ${entry.name} (unknown type: ${ext})`);
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1130
|
-
}
|
|
733
|
+
_getFileIcon(ext) {
|
|
734
|
+
const icons = {
|
|
735
|
+
'.html': '📄',
|
|
736
|
+
'.css': '🎨',
|
|
737
|
+
'.js': '📜',
|
|
738
|
+
'.json': '📋',
|
|
739
|
+
'.png': '🖼️',
|
|
740
|
+
'.jpg': '🖼️',
|
|
741
|
+
'.jpeg': '🖼️',
|
|
742
|
+
'.gif': '🖼️',
|
|
743
|
+
'.svg': '🎨',
|
|
744
|
+
'.ico': '🔖',
|
|
745
|
+
'.woff': '🔤',
|
|
746
|
+
'.woff2': '🔤',
|
|
747
|
+
'.ttf': '🔤',
|
|
748
|
+
'.eot': '🔤'
|
|
1131
749
|
};
|
|
1132
|
-
|
|
1133
|
-
copyRecursive(publicPath, distDir);
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
/**
|
|
1137
|
-
* Main build pipeline
|
|
1138
|
-
*/
|
|
1139
|
-
async build() {
|
|
1140
|
-
console.log('🔨 Building JUX project...\n');
|
|
1141
|
-
|
|
1142
|
-
try {
|
|
1143
|
-
// 1. Clean dist
|
|
1144
|
-
if (fs.existsSync(this.config.distDir)) {
|
|
1145
|
-
fs.rmSync(this.config.distDir, { recursive: true, force: true });
|
|
1146
|
-
}
|
|
1147
|
-
fs.mkdirSync(this.config.distDir, { recursive: true });
|
|
1148
|
-
|
|
1149
|
-
// 2. Scan .jux files
|
|
1150
|
-
const juxFiles = this.scanJuxFiles(this.config.srcDir);
|
|
1151
|
-
console.log(`📂 Found ${juxFiles.length} .jux files\n`);
|
|
1152
|
-
|
|
1153
|
-
// 3. ✅ Bundle all .jux files → entry.js
|
|
1154
|
-
await this.bundleJuxFiles(juxFiles);
|
|
1155
|
-
|
|
1156
|
-
// 4. ✅ Bundle vendor libraries → bundle.js
|
|
1157
|
-
await this.bundleVendorFiles();
|
|
1158
|
-
|
|
1159
|
-
// 5. Copy public assets (CSS, images, etc.)
|
|
1160
|
-
await this.copyPublicAssets();
|
|
1161
|
-
|
|
1162
|
-
// 6. Generate index.html (with entry.js + bundle.js)
|
|
1163
|
-
await this.generateIndexHtml();
|
|
1164
|
-
|
|
1165
|
-
console.log('\n✅ Build completed successfully!\n');
|
|
1166
|
-
console.log(`📁 Output: ${this.config.distDir}`);
|
|
1167
|
-
console.log(` ✓ entry.js (${juxFiles.length} .jux files bundled)`);
|
|
1168
|
-
console.log(` ✓ bundle.js (vendor libraries)`);
|
|
1169
|
-
console.log(` ✓ index.html`);
|
|
1170
|
-
console.log(` ✓ Public assets copied\n`);
|
|
1171
|
-
|
|
1172
|
-
return { success: true };
|
|
1173
|
-
|
|
1174
|
-
} catch (error) {
|
|
1175
|
-
console.error('\n❌ Build failed:', error.message);
|
|
1176
|
-
console.error(error.stack);
|
|
1177
|
-
return { success: false, error };
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
/**
|
|
1182
|
-
* Scan for .jux files ONLY in source directory
|
|
1183
|
-
*/
|
|
1184
|
-
scanJuxFiles(dir) {
|
|
1185
|
-
const juxFiles = [];
|
|
1186
|
-
|
|
1187
|
-
const scan = (currentDir) => {
|
|
1188
|
-
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
1189
|
-
|
|
1190
|
-
for (const entry of entries) {
|
|
1191
|
-
const fullPath = path.join(currentDir, entry.name);
|
|
1192
|
-
|
|
1193
|
-
if (entry.isDirectory()) {
|
|
1194
|
-
scan(fullPath);
|
|
1195
|
-
} else if (entry.name.endsWith('.jux')) {
|
|
1196
|
-
juxFiles.push(fullPath);
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
};
|
|
1200
|
-
|
|
1201
|
-
scan(dir);
|
|
1202
|
-
return juxFiles;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
/**
|
|
1206
|
-
* Compile a single .jux file to .js
|
|
1207
|
-
*/
|
|
1208
|
-
async compileFile(juxFilePath) {
|
|
1209
|
-
const relativePath = path.relative(this.config.srcDir, juxFilePath);
|
|
1210
|
-
const outputPath = path.join(this.config.distDir, relativePath.replace(/\.jux$/, '.js'));
|
|
1211
|
-
|
|
1212
|
-
// Ensure output directory exists
|
|
1213
|
-
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1214
|
-
|
|
1215
|
-
// Read .jux source
|
|
1216
|
-
const juxCode = fs.readFileSync(juxFilePath, 'utf8');
|
|
1217
|
-
|
|
1218
|
-
// Compile to JavaScript
|
|
1219
|
-
const jsCode = this.transformJuxToJs(juxCode);
|
|
1220
|
-
|
|
1221
|
-
// Write compiled .js file
|
|
1222
|
-
fs.writeFileSync(outputPath, jsCode, 'utf8');
|
|
1223
|
-
|
|
1224
|
-
console.log(` ✓ ${relativePath} → ${path.basename(outputPath)}`);
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
/**
|
|
1228
|
-
* ✅ Generate name from folder structure
|
|
1229
|
-
* Example: abc/juxabc.jux -> abc_juxabc
|
|
1230
|
-
*/
|
|
1231
|
-
_generateNameFromPath(filepath) {
|
|
1232
|
-
// Convert file path to module name
|
|
1233
|
-
// e.g., "pages/about.jux" → "pages_about"
|
|
1234
|
-
return filepath
|
|
1235
|
-
.replace(/\.jux$/, '')
|
|
1236
|
-
.replace(/[\/\\]/g, '_')
|
|
1237
|
-
.replace(/[^a-zA-Z0-9_]/g, '');
|
|
750
|
+
return icons[ext.toLowerCase()] || '📦';
|
|
1238
751
|
}
|
|
1239
752
|
}
|