juxscript 1.1.117 → 1.1.120
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 +156 -812
- package/package.json +1 -1
package/dom-structure-map.json
CHANGED
package/machinery/compiler3.js
CHANGED
|
@@ -13,7 +13,7 @@ export class JuxCompiler {
|
|
|
13
13
|
this.config = config;
|
|
14
14
|
this.srcDir = config.srcDir || './jux';
|
|
15
15
|
this.distDir = config.distDir || './.jux-dist';
|
|
16
|
-
this.publicDir = config.publicDir || './public';
|
|
16
|
+
this.publicDir = config.publicDir || './public'; // ✅ Configurable public path
|
|
17
17
|
this.defaults = config.defaults || {};
|
|
18
18
|
this.paths = config.paths || {};
|
|
19
19
|
this._juxscriptExports = null;
|
|
@@ -53,56 +53,24 @@ export class JuxCompiler {
|
|
|
53
53
|
return null;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
scanFiles(dir = this.srcDir, baseDir = this.srcDir) {
|
|
60
|
-
const views = [];
|
|
61
|
-
const dataModules = [];
|
|
62
|
-
const sharedModules = [];
|
|
56
|
+
scanFiles() {
|
|
57
|
+
const files = fs.readdirSync(this.srcDir)
|
|
58
|
+
.filter(f => (f.endsWith('.jux') || f.endsWith('.js')) && !this.isAssetFile(f));
|
|
63
59
|
|
|
64
|
-
const
|
|
60
|
+
const views = [], dataModules = [], sharedModules = [];
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
62
|
+
files.forEach(file => {
|
|
63
|
+
const content = fs.readFileSync(path.join(this.srcDir, file), 'utf8');
|
|
64
|
+
const name = file.replace(/\.[^/.]+$/, '');
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
views.push(...nested.views);
|
|
77
|
-
dataModules.push(...nested.dataModules);
|
|
78
|
-
sharedModules.push(...nested.sharedModules);
|
|
79
|
-
} else if ((entry.name.endsWith('.jux') || entry.name.endsWith('.js')) && !this.isAssetFile(entry.name)) {
|
|
80
|
-
const content = fs.readFileSync(fullPath, 'utf8');
|
|
81
|
-
|
|
82
|
-
// ✅ Generate name from folder structure
|
|
83
|
-
// Example: abc/juxabc.jux -> abc_juxabc
|
|
84
|
-
const nameFromPath = relativePath
|
|
85
|
-
.replace(/\.[^/.]+$/, '') // Remove extension
|
|
86
|
-
.replace(/\\/g, '/') // Normalize Windows paths
|
|
87
|
-
.replace(/\//g, '_') // Folder separator -> underscore
|
|
88
|
-
.replace(/[^a-zA-Z0-9_]/g, '_'); // Sanitize
|
|
89
|
-
|
|
90
|
-
const module = {
|
|
91
|
-
name: nameFromPath,
|
|
92
|
-
file: relativePath,
|
|
93
|
-
fullPath: fullPath,
|
|
94
|
-
content: content
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
if (entry.name.includes('data')) {
|
|
98
|
-
dataModules.push(module);
|
|
99
|
-
} else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
|
|
100
|
-
sharedModules.push(module);
|
|
101
|
-
} else {
|
|
102
|
-
views.push(module);
|
|
103
|
-
}
|
|
66
|
+
if (file.includes('data')) {
|
|
67
|
+
dataModules.push({ name, file, content });
|
|
68
|
+
} else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
|
|
69
|
+
sharedModules.push({ name, file, content });
|
|
70
|
+
} else {
|
|
71
|
+
views.push({ name, file, content });
|
|
104
72
|
}
|
|
105
|
-
}
|
|
73
|
+
});
|
|
106
74
|
|
|
107
75
|
return { views, dataModules, sharedModules };
|
|
108
76
|
}
|
|
@@ -203,46 +171,7 @@ export class JuxCompiler {
|
|
|
203
171
|
}
|
|
204
172
|
|
|
205
173
|
/**
|
|
206
|
-
*
|
|
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
|
|
174
|
+
* Generate entry point without layout/theme logic
|
|
246
175
|
*/
|
|
247
176
|
generateEntryPoint(views, dataModules, sharedModules) {
|
|
248
177
|
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
@@ -250,36 +179,18 @@ export class JuxCompiler {
|
|
|
250
179
|
const sourceSnapshot = {};
|
|
251
180
|
|
|
252
181
|
const juxImports = new Set();
|
|
253
|
-
const layoutImports = new Set(); // ✅ Track layout imports separately
|
|
254
|
-
|
|
255
|
-
// Scan for imports
|
|
256
182
|
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
257
|
-
// Regular juxscript imports
|
|
258
183
|
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
259
184
|
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
|
-
}
|
|
185
|
+
if (imp) juxImports.add(imp);
|
|
268
186
|
});
|
|
269
187
|
}
|
|
270
188
|
});
|
|
271
189
|
|
|
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
190
|
if (juxImports.size > 0) {
|
|
279
191
|
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
280
192
|
}
|
|
281
193
|
|
|
282
|
-
// Data and shared modules
|
|
283
194
|
dataModules.forEach(m => {
|
|
284
195
|
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
285
196
|
});
|
|
@@ -291,24 +202,14 @@ export class JuxCompiler {
|
|
|
291
202
|
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
292
203
|
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
293
204
|
|
|
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
205
|
if (juxImports.size > 0) {
|
|
304
|
-
entry += `\
|
|
305
|
-
entry += `Object.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
206
|
+
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
306
207
|
}
|
|
307
208
|
|
|
308
209
|
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
309
210
|
|
|
310
211
|
views.forEach(v => {
|
|
311
|
-
const
|
|
212
|
+
const capitalized = v.name.charAt(0).toUpperCase() + v.name.slice(1);
|
|
312
213
|
allIssues.push(...this.validateViewCode(v.name, v.content));
|
|
313
214
|
|
|
314
215
|
sourceSnapshot[v.file] = {
|
|
@@ -321,7 +222,7 @@ export class JuxCompiler {
|
|
|
321
222
|
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
322
223
|
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
323
224
|
|
|
324
|
-
entry += `\n${asyncPrefix}function render${
|
|
225
|
+
entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
|
|
325
226
|
});
|
|
326
227
|
|
|
327
228
|
dataModules.forEach(m => {
|
|
@@ -338,26 +239,28 @@ export class JuxCompiler {
|
|
|
338
239
|
}
|
|
339
240
|
|
|
340
241
|
reportValidationIssues() {
|
|
341
|
-
|
|
242
|
+
const issues = this._validationIssues || [];
|
|
243
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
244
|
+
const warnings = issues.filter(i => i.type === 'warning');
|
|
245
|
+
|
|
246
|
+
if (issues.length > 0) {
|
|
247
|
+
console.log('\n⚠️ Validation Issues:\n');
|
|
248
|
+
issues.forEach(issue => {
|
|
249
|
+
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
250
|
+
console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
|
|
251
|
+
});
|
|
252
|
+
console.log('');
|
|
253
|
+
}
|
|
342
254
|
|
|
343
|
-
|
|
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('');
|
|
255
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
349
256
|
}
|
|
350
257
|
|
|
351
|
-
/**
|
|
352
|
-
* ✅ Generate routes based on folder structure (SINGLE DEFINITION)
|
|
353
|
-
*/
|
|
354
258
|
_generateRouter(views) {
|
|
355
259
|
let routeMap = '';
|
|
356
|
-
|
|
357
260
|
views.forEach(v => {
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
routeMap += ` '
|
|
261
|
+
const cap = v.name.charAt(0).toUpperCase() + v.name.slice(1);
|
|
262
|
+
if (v.name.toLowerCase() === 'index') routeMap += ` '/': render${cap},\n`;
|
|
263
|
+
routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
|
|
361
264
|
});
|
|
362
265
|
|
|
363
266
|
return `
|
|
@@ -375,17 +278,12 @@ async function __juxLoadSources() {
|
|
|
375
278
|
}
|
|
376
279
|
|
|
377
280
|
function __juxFindSource(stack) {
|
|
378
|
-
var match = stack.match(/render(
|
|
281
|
+
var match = stack.match(/render(\\w+)/);
|
|
379
282
|
if (match) {
|
|
380
|
-
var
|
|
283
|
+
var viewName = match[1].toLowerCase();
|
|
381
284
|
for (var file in __juxSources || {}) {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
.map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
|
|
385
|
-
.join('');
|
|
386
|
-
|
|
387
|
-
if (normalized === funcName) {
|
|
388
|
-
return { file: file, source: __juxSources[file], viewName: funcName };
|
|
285
|
+
if (__juxSources[file].name.toLowerCase() === viewName) {
|
|
286
|
+
return { file: file, source: __juxSources[file], viewName: match[1] };
|
|
389
287
|
}
|
|
390
288
|
}
|
|
391
289
|
}
|
|
@@ -488,8 +386,7 @@ window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error ||
|
|
|
488
386
|
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
489
387
|
|
|
490
388
|
// --- JUX ROUTER ---
|
|
491
|
-
const routes = {
|
|
492
|
-
${routeMap}};
|
|
389
|
+
const routes = {\n${routeMap}};
|
|
493
390
|
|
|
494
391
|
async function navigate(path) {
|
|
495
392
|
const view = routes[path];
|
|
@@ -517,723 +414,170 @@ navigate(location.pathname);
|
|
|
517
414
|
`;
|
|
518
415
|
}
|
|
519
416
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
*/
|
|
523
|
-
_copySourceToDist() {
|
|
524
|
-
const distJuxDir = path.join(this.distDir, 'jux');
|
|
417
|
+
async build() {
|
|
418
|
+
console.log('🚀 JUX Build\n');
|
|
525
419
|
|
|
526
|
-
|
|
527
|
-
if (!
|
|
528
|
-
|
|
420
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
421
|
+
if (!juxscriptPath) {
|
|
422
|
+
console.error('❌ Could not locate juxscript package');
|
|
423
|
+
return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
|
|
529
424
|
}
|
|
425
|
+
console.log(`📦 Using: ${juxscriptPath}`);
|
|
530
426
|
|
|
531
|
-
|
|
532
|
-
this._copySourceFilesRecursive(this.srcDir, this.srcDir, distJuxDir);
|
|
533
|
-
}
|
|
427
|
+
await this.loadJuxscriptExports();
|
|
534
428
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
429
|
+
if (fs.existsSync(this.distDir)) {
|
|
430
|
+
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
431
|
+
}
|
|
432
|
+
fs.mkdirSync(this.distDir, { recursive: true });
|
|
540
433
|
|
|
541
|
-
|
|
542
|
-
|
|
434
|
+
// ✅ Copy public folder if exists
|
|
435
|
+
this.copyPublicFolder();
|
|
543
436
|
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
const destPath = path.join(distBase, relativePath);
|
|
437
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
438
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
547
439
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
-
}
|
|
440
|
+
// Copy data/shared modules to dist
|
|
441
|
+
const juxDistDir = path.join(this.distDir, 'jux');
|
|
442
|
+
fs.mkdirSync(juxDistDir, { recursive: true });
|
|
443
|
+
[...dataModules, ...sharedModules].forEach(m => {
|
|
444
|
+
fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
|
|
567
445
|
});
|
|
568
|
-
}
|
|
569
446
|
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
*/
|
|
574
|
-
async copyPublicAssets() {
|
|
575
|
-
const { publicDir, distDir, paths } = this.config;
|
|
447
|
+
const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
448
|
+
const entryPath = path.join(this.distDir, 'entry.js');
|
|
449
|
+
fs.writeFileSync(entryPath, entryContent);
|
|
576
450
|
|
|
577
|
-
|
|
578
|
-
|
|
451
|
+
const snapshotPath = path.join(this.distDir, '__jux_sources.json');
|
|
452
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
453
|
+
console.log(`📸 Source snapshot written`);
|
|
579
454
|
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
455
|
+
const validation = this.reportValidationIssues();
|
|
456
|
+
if (!validation.isValid) {
|
|
457
|
+
console.log('🛑 BUILD FAILED\n');
|
|
458
|
+
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
583
459
|
}
|
|
584
460
|
|
|
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
|
|
634
|
-
*/
|
|
635
|
-
async build() {
|
|
636
|
-
console.log('🔨 Building JUX project...\n');
|
|
637
|
-
|
|
638
461
|
try {
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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`);
|
|
667
|
-
|
|
668
|
-
return { success: true };
|
|
669
|
-
|
|
670
|
-
} catch (error) {
|
|
671
|
-
console.error('\n❌ Build failed:', error.message);
|
|
672
|
-
console.error(error.stack);
|
|
673
|
-
return { success: false, error };
|
|
674
|
-
}
|
|
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
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Compile a single .jux file to .js
|
|
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
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* ✅ Generate name from folder structure
|
|
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
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
/**
|
|
736
|
-
* ✅ Generate PascalCase function name from sanitized name
|
|
737
|
-
* Examples:
|
|
738
|
-
* abc_juxabc -> AbcJuxabc
|
|
739
|
-
* index -> Index
|
|
740
|
-
*/
|
|
741
|
-
_generateFunctionName(name) {
|
|
742
|
-
return name
|
|
743
|
-
.split('_')
|
|
744
|
-
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
745
|
-
.join('');
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
/**
|
|
749
|
-
* ✅ Generate entry point with nested folder support
|
|
750
|
-
*/
|
|
751
|
-
generateEntryPoint(views, dataModules, sharedModules) {
|
|
752
|
-
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
753
|
-
const allIssues = [];
|
|
754
|
-
const sourceSnapshot = {};
|
|
755
|
-
|
|
756
|
-
const juxImports = new Set();
|
|
757
|
-
const layoutImports = new Set(); // ✅ Track layout imports separately
|
|
758
|
-
|
|
759
|
-
// Scan for imports
|
|
760
|
-
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
761
|
-
// Regular juxscript imports
|
|
762
|
-
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
763
|
-
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
|
-
}
|
|
462
|
+
await esbuild.build({
|
|
463
|
+
entryPoints: [entryPath],
|
|
464
|
+
bundle: true,
|
|
465
|
+
outfile: path.join(this.distDir, 'bundle.js'),
|
|
466
|
+
format: 'esm',
|
|
467
|
+
platform: 'browser',
|
|
468
|
+
target: 'esnext',
|
|
469
|
+
sourcemap: true,
|
|
470
|
+
loader: { '.jux': 'js', '.css': 'empty' },
|
|
471
|
+
plugins: [{
|
|
472
|
+
name: 'juxscript-resolver',
|
|
473
|
+
setup: (build) => {
|
|
474
|
+
build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
|
|
771
475
|
}
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
});
|
|
775
|
-
|
|
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
|
-
if (juxImports.size > 0) {
|
|
783
|
-
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
// Data and shared modules
|
|
787
|
-
dataModules.forEach(m => {
|
|
788
|
-
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
789
|
-
});
|
|
790
|
-
sharedModules.forEach(m => {
|
|
791
|
-
entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
|
|
792
|
-
});
|
|
793
|
-
|
|
794
|
-
entry += `\n// Expose to window\n`;
|
|
795
|
-
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
796
|
-
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
797
|
-
|
|
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`;
|
|
476
|
+
}],
|
|
803
477
|
});
|
|
478
|
+
console.log('✅ esbuild complete');
|
|
479
|
+
} catch (err) {
|
|
480
|
+
console.error('❌ esbuild failed:', err);
|
|
481
|
+
return { success: false, errors: [{ message: err.message }], warnings: [] };
|
|
804
482
|
}
|
|
805
483
|
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
484
|
+
const html = `<!DOCTYPE html>
|
|
485
|
+
<html lang="en">
|
|
486
|
+
<head>
|
|
487
|
+
<meta charset="UTF-8">
|
|
488
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
489
|
+
<title>JUX App</title>
|
|
490
|
+
<script type="module" src="./bundle.js"></script>
|
|
491
|
+
</head>
|
|
492
|
+
<body>
|
|
493
|
+
<div id="app"></div>
|
|
494
|
+
</body>
|
|
495
|
+
</html>`;
|
|
496
|
+
fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
|
|
813
497
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
allIssues.push(...this.validateViewCode(v.name, v.content));
|
|
498
|
+
fs.unlinkSync(entryPath);
|
|
499
|
+
fs.rmSync(juxDistDir, { recursive: true, force: true });
|
|
817
500
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
file: v.file,
|
|
821
|
-
content: v.content,
|
|
822
|
-
lines: v.content.split('\n')
|
|
823
|
-
};
|
|
824
|
-
|
|
825
|
-
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
826
|
-
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
827
|
-
|
|
828
|
-
entry += `\n${asyncPrefix}function render${functionName}() {\n${viewCode}\n}\n`;
|
|
829
|
-
});
|
|
830
|
-
|
|
831
|
-
dataModules.forEach(m => {
|
|
832
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
833
|
-
});
|
|
834
|
-
sharedModules.forEach(m => {
|
|
835
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
836
|
-
});
|
|
837
|
-
|
|
838
|
-
this._sourceSnapshot = sourceSnapshot;
|
|
839
|
-
this._validationIssues = allIssues;
|
|
840
|
-
entry += this._generateRouter(views);
|
|
841
|
-
return entry;
|
|
842
|
-
}
|
|
843
|
-
|
|
844
|
-
reportValidationIssues() {
|
|
845
|
-
if (!this._validationIssues || this._validationIssues.length === 0) return;
|
|
846
|
-
|
|
847
|
-
console.log('\n⚠️ Validation Issues:\n');
|
|
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('');
|
|
501
|
+
console.log(`\n✅ Build Complete!\n`);
|
|
502
|
+
return { success: true, errors: [], warnings: validation.warnings };
|
|
853
503
|
}
|
|
854
504
|
|
|
855
505
|
/**
|
|
856
|
-
*
|
|
506
|
+
* Copy public folder contents to dist
|
|
857
507
|
*/
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
return `
|
|
868
|
-
// --- JUX SOURCE LOADER ---
|
|
869
|
-
var __juxSources = null;
|
|
870
|
-
async function __juxLoadSources() {
|
|
871
|
-
if (__juxSources) return __juxSources;
|
|
872
|
-
try {
|
|
873
|
-
var res = await fetch('/__jux_sources.json');
|
|
874
|
-
__juxSources = await res.json();
|
|
875
|
-
} catch (e) {
|
|
876
|
-
__juxSources = {};
|
|
877
|
-
}
|
|
878
|
-
return __juxSources;
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
function __juxFindSource(stack) {
|
|
882
|
-
var match = stack.match(/render([A-Z][a-zA-Z0-9_]*)/);
|
|
883
|
-
if (match) {
|
|
884
|
-
var funcName = match[1];
|
|
885
|
-
for (var file in __juxSources || {}) {
|
|
886
|
-
var normalized = __juxSources[file].name
|
|
887
|
-
.split('_')
|
|
888
|
-
.map(s => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase())
|
|
889
|
-
.join('');
|
|
890
|
-
|
|
891
|
-
if (normalized === funcName) {
|
|
892
|
-
return { file: file, source: __juxSources[file], viewName: funcName };
|
|
893
|
-
}
|
|
508
|
+
copyPublicFolder() {
|
|
509
|
+
// ✅ Use configured public path or resolve from paths object
|
|
510
|
+
const publicSrc = this.paths.public
|
|
511
|
+
? this.paths.public
|
|
512
|
+
: path.resolve(process.cwd(), this.publicDir);
|
|
513
|
+
|
|
514
|
+
if (!fs.existsSync(publicSrc)) {
|
|
515
|
+
return; // No public folder, skip
|
|
894
516
|
}
|
|
895
|
-
}
|
|
896
|
-
return null;
|
|
897
|
-
}
|
|
898
517
|
|
|
899
|
-
|
|
900
|
-
var __juxErrorOverlay = {
|
|
901
|
-
styles: \`
|
|
902
|
-
#__jux-error-overlay {
|
|
903
|
-
position: fixed; inset: 0; z-index: 99999;
|
|
904
|
-
background: rgba(0, 0, 0, 0.4);
|
|
905
|
-
display: flex; align-items: center; justify-content: center;
|
|
906
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
907
|
-
opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
|
|
908
|
-
}
|
|
909
|
-
#__jux-error-overlay.visible { opacity: 1; }
|
|
910
|
-
#__jux-error-overlay * { box-sizing: border-box; }
|
|
911
|
-
.__jux-modal {
|
|
912
|
-
background: #f8f9fa; border-radius: 4px;
|
|
913
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
914
|
-
max-width: 80vw; width: 90%; max-height: 90vh;
|
|
915
|
-
overflow: hidden; display: flex; flex-direction: column;
|
|
916
|
-
transform: translateY(10px); transition: transform 0.2s ease-out;
|
|
917
|
-
}
|
|
918
|
-
#__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
|
|
919
|
-
.__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
|
|
920
|
-
.__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
|
|
921
|
-
.__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
|
|
922
|
-
.__jux-header .file-info { color: #6b7280; font-size: 13px; }
|
|
923
|
-
.__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
|
|
924
|
-
.__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
|
|
925
|
-
.__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
|
|
926
|
-
.__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
|
|
927
|
-
.__jux-code-line.error { background: #fef2f2; }
|
|
928
|
-
.__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
|
|
929
|
-
.__jux-code-line.context { background: #fefce8; }
|
|
930
|
-
.__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
|
|
931
|
-
.__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
|
|
932
|
-
.__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
|
|
933
|
-
.__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
|
|
934
|
-
.__jux-tip { color: #6b7280; font-size: 12px; }
|
|
935
|
-
.__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
936
|
-
.__jux-dismiss:hover { background: #e5e7eb; }
|
|
937
|
-
.__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
|
|
938
|
-
.__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; }
|
|
939
|
-
\`,
|
|
518
|
+
console.log('📦 Copying public assets...');
|
|
940
519
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
var overlay = document.createElement('div');
|
|
948
|
-
overlay.id = '__jux-error-overlay';
|
|
949
|
-
var stack = error && error.stack ? error.stack : '';
|
|
950
|
-
var msg = error && error.message ? error.message : String(error);
|
|
951
|
-
var found = __juxFindSource(stack);
|
|
952
|
-
var sourceHtml = '', fileInfo = '';
|
|
953
|
-
|
|
954
|
-
if (found && found.source && found.source.lines) {
|
|
955
|
-
var lines = found.source.lines;
|
|
956
|
-
fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
|
|
957
|
-
var errorLineIndex = -1;
|
|
958
|
-
var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
|
|
959
|
-
if (errorMethod) {
|
|
960
|
-
for (var i = 0; i < lines.length; i++) {
|
|
961
|
-
if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
|
|
962
|
-
}
|
|
963
|
-
}
|
|
964
|
-
if (errorLineIndex === -1) {
|
|
965
|
-
for (var i = 0; i < lines.length; i++) {
|
|
966
|
-
if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
|
|
967
|
-
}
|
|
968
|
-
}
|
|
969
|
-
var contextStart = Math.max(0, errorLineIndex - 3);
|
|
970
|
-
var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
|
|
971
|
-
if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
|
|
972
|
-
|
|
973
|
-
for (var i = contextStart; i <= contextEnd; i++) {
|
|
974
|
-
var isError = (i === errorLineIndex);
|
|
975
|
-
var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
|
|
976
|
-
var lineClass = isError ? ' error' : (isContext ? ' context' : '');
|
|
977
|
-
var lineCode = lines[i].replace(/</g, '<').replace(/>/g, '>') || ' ';
|
|
978
|
-
sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
|
|
979
|
-
}
|
|
980
|
-
} else {
|
|
981
|
-
sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
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>';
|
|
985
|
-
document.body.appendChild(overlay);
|
|
986
|
-
requestAnimationFrame(function() { overlay.classList.add('visible'); });
|
|
987
|
-
console.error(title + ':', error);
|
|
988
|
-
}
|
|
989
|
-
};
|
|
990
|
-
|
|
991
|
-
window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
|
|
992
|
-
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
993
|
-
|
|
994
|
-
// --- JUX ROUTER ---
|
|
995
|
-
const routes = {
|
|
996
|
-
${routeMap}};
|
|
997
|
-
|
|
998
|
-
async function navigate(path) {
|
|
999
|
-
const view = routes[path];
|
|
1000
|
-
if (!view) {
|
|
1001
|
-
document.getElementById('app').innerHTML = '<h1 style="padding:40px;">404 - Not Found</h1>';
|
|
1002
|
-
return;
|
|
1003
|
-
}
|
|
1004
|
-
document.getElementById('app').innerHTML = '';
|
|
1005
|
-
var overlay = document.getElementById('__jux-error-overlay');
|
|
1006
|
-
if (overlay) overlay.remove();
|
|
1007
|
-
try { await view(); } catch (err) { __juxErrorOverlay.show(err, 'Jux Render Error'); }
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
document.addEventListener('click', e => {
|
|
1011
|
-
const a = e.target.closest('a');
|
|
1012
|
-
if (!a || a.dataset.router === 'false') return;
|
|
1013
|
-
try { if (new URL(a.href, location.origin).origin !== location.origin) return; } catch { return; }
|
|
1014
|
-
e.preventDefault();
|
|
1015
|
-
history.pushState({}, '', a.href);
|
|
1016
|
-
navigate(new URL(a.href, location.origin).pathname);
|
|
1017
|
-
});
|
|
1018
|
-
|
|
1019
|
-
window.addEventListener('popstate', () => navigate(location.pathname));
|
|
1020
|
-
navigate(location.pathname);
|
|
1021
|
-
`;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
/**
|
|
1025
|
-
* ✅ Copy source files to dist/jux for esbuild to resolve
|
|
1026
|
-
*/
|
|
1027
|
-
_copySourceToDist() {
|
|
1028
|
-
const distJuxDir = path.join(this.distDir, 'jux');
|
|
1029
|
-
|
|
1030
|
-
// Ensure dist/jux directory exists
|
|
1031
|
-
if (!fs.existsSync(distJuxDir)) {
|
|
1032
|
-
fs.mkdirSync(distJuxDir, { recursive: true });
|
|
520
|
+
try {
|
|
521
|
+
this._copyDirRecursive(publicSrc, this.distDir, 0);
|
|
522
|
+
console.log('✅ Public assets copied');
|
|
523
|
+
} catch (err) {
|
|
524
|
+
console.warn('⚠️ Error copying public folder:', err.message);
|
|
1033
525
|
}
|
|
1034
|
-
|
|
1035
|
-
// Copy all .jux and .js files from source to dist
|
|
1036
|
-
this._copySourceFilesRecursive(this.srcDir, this.srcDir, distJuxDir);
|
|
1037
526
|
}
|
|
1038
527
|
|
|
1039
528
|
/**
|
|
1040
|
-
* Recursively copy
|
|
529
|
+
* Recursively copy directory contents
|
|
1041
530
|
*/
|
|
1042
|
-
|
|
531
|
+
_copyDirRecursive(src, dest, depth = 0) {
|
|
1043
532
|
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
1044
533
|
|
|
1045
534
|
entries.forEach(entry => {
|
|
1046
|
-
|
|
535
|
+
// Skip hidden files and directories
|
|
536
|
+
if (entry.name.startsWith('.')) return;
|
|
1047
537
|
|
|
1048
538
|
const srcPath = path.join(src, entry.name);
|
|
1049
|
-
const
|
|
1050
|
-
const destPath = path.join(distBase, relativePath);
|
|
539
|
+
const destPath = path.join(dest, entry.name);
|
|
1051
540
|
|
|
1052
541
|
if (entry.isDirectory()) {
|
|
1053
|
-
// Create directory
|
|
542
|
+
// Create directory and recurse
|
|
1054
543
|
if (!fs.existsSync(destPath)) {
|
|
1055
544
|
fs.mkdirSync(destPath, { recursive: true });
|
|
1056
545
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
}
|
|
1067
|
-
fs.copyFileSync(srcPath, destPath);
|
|
1068
|
-
console.log(` 📋 Copied: ${relativePath}`);
|
|
546
|
+
this._copyDirRecursive(srcPath, destPath, depth + 1);
|
|
547
|
+
} else {
|
|
548
|
+
// Copy file
|
|
549
|
+
fs.copyFileSync(srcPath, destPath);
|
|
550
|
+
|
|
551
|
+
// Log files at root level only
|
|
552
|
+
if (depth === 0) {
|
|
553
|
+
const ext = path.extname(entry.name);
|
|
554
|
+
const icon = this._getFileIcon(ext);
|
|
555
|
+
console.log(` ${icon} ${entry.name}`);
|
|
1069
556
|
}
|
|
1070
557
|
}
|
|
1071
558
|
});
|
|
1072
559
|
}
|
|
1073
560
|
|
|
1074
561
|
/**
|
|
1075
|
-
*
|
|
1076
|
-
* ❌ Does NOT copy .jux files (they are compiled separately)
|
|
562
|
+
* Get icon for file type
|
|
1077
563
|
*/
|
|
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
|
-
}
|
|
1131
|
-
};
|
|
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
|
-
}
|
|
564
|
+
_getFileIcon(ext) {
|
|
565
|
+
const icons = {
|
|
566
|
+
'.html': '📄',
|
|
567
|
+
'.css': '🎨',
|
|
568
|
+
'.js': '📜',
|
|
569
|
+
'.json': '📋',
|
|
570
|
+
'.png': '🖼️',
|
|
571
|
+
'.jpg': '🖼️',
|
|
572
|
+
'.jpeg': '🖼️',
|
|
573
|
+
'.gif': '🖼️',
|
|
574
|
+
'.svg': '🎨',
|
|
575
|
+
'.ico': '🔖',
|
|
576
|
+
'.woff': '🔤',
|
|
577
|
+
'.woff2': '🔤',
|
|
578
|
+
'.ttf': '🔤',
|
|
579
|
+
'.eot': '🔤'
|
|
1199
580
|
};
|
|
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, '');
|
|
581
|
+
return icons[ext.toLowerCase()] || '📦';
|
|
1238
582
|
}
|
|
1239
583
|
}
|