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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "totalComponents": 69,
3
- "generatedAt": "2026-02-13T04:55:13.466Z",
3
+ "generatedAt": "2026-02-13T05:01:41.327Z",
4
4
  "components": [
5
5
  {
6
6
  "file": "alert.js",
@@ -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
- * Recursively scan for .jux and .js files
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 entries = fs.readdirSync(dir, { withFileTypes: true });
60
+ const views = [], dataModules = [], sharedModules = [];
65
61
 
66
- for (const entry of entries) {
67
- // Skip hidden files/folders
68
- if (entry.name.startsWith('.')) continue;
62
+ files.forEach(file => {
63
+ const content = fs.readFileSync(path.join(this.srcDir, file), 'utf8');
64
+ const name = file.replace(/\.[^/.]+$/, '');
69
65
 
70
- const fullPath = path.join(dir, entry.name);
71
- const relativePath = path.relative(baseDir, fullPath);
72
-
73
- if (entry.isDirectory()) {
74
- // Recurse into subdirectories
75
- const nested = this.scanFiles(fullPath, baseDir);
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
- * Generate route path from file path
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 += `\n// Expose components\n`;
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 functionName = this._generateFunctionName(v.name);
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${functionName}() {\n${viewCode}\n}\n`;
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
- if (!this._validationIssues || this._validationIssues.length === 0) return;
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
- 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('');
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 routePath = this._generateRoutePath(v.file);
359
- const functionName = this._generateFunctionName(v.name);
360
- routeMap += ` '${routePath}': render${functionName},\n`;
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([A-Z][a-zA-Z0-9_]*)/);
281
+ var match = stack.match(/render(\\w+)/);
379
282
  if (match) {
380
- var funcName = match[1];
283
+ var viewName = match[1].toLowerCase();
381
284
  for (var file in __juxSources || {}) {
382
- var normalized = __juxSources[file].name
383
- .split('_')
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
- * Copy source files to dist/jux for esbuild to resolve
522
- */
523
- _copySourceToDist() {
524
- const distJuxDir = path.join(this.distDir, 'jux');
417
+ async build() {
418
+ console.log('🚀 JUX Build\n');
525
419
 
526
- // Ensure dist/jux directory exists
527
- if (!fs.existsSync(distJuxDir)) {
528
- fs.mkdirSync(distJuxDir, { recursive: true });
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
- // Copy all .jux and .js files from source to dist
532
- this._copySourceFilesRecursive(this.srcDir, this.srcDir, distJuxDir);
533
- }
427
+ await this.loadJuxscriptExports();
534
428
 
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 });
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
- entries.forEach(entry => {
542
- if (entry.name.startsWith('.')) return; // Skip hidden
434
+ // Copy public folder if exists
435
+ this.copyPublicFolder();
543
436
 
544
- const srcPath = path.join(src, entry.name);
545
- const relativePath = path.relative(baseDir, srcPath);
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
- 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
- }
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
- * 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;
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
- // Resolve public folder path
578
- const publicPath = paths?.public || path.resolve(this.config.srcDir, '..', publicDir);
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
- if (!fs.existsSync(publicPath)) {
581
- console.log(`ℹ️ No public folder found at ${publicPath}, skipping asset copy`);
582
- return;
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
- // 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`);
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
- // Expose regular components
807
- if (juxImports.size > 0) {
808
- entry += `\n// Expose components\n`;
809
- entry += `Object.assign(window, { ${[...juxImports].join(', ')} });\n`;
810
- }
811
-
812
- entry += `\n// --- VIEW FUNCTIONS ---\n`;
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
- views.forEach(v => {
815
- const functionName = this._generateFunctionName(v.name);
816
- allIssues.push(...this.validateViewCode(v.name, v.content));
498
+ fs.unlinkSync(entryPath);
499
+ fs.rmSync(juxDistDir, { recursive: true, force: true });
817
500
 
818
- sourceSnapshot[v.file] = {
819
- name: v.name,
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
- * Generate routes based on folder structure (SINGLE DEFINITION)
506
+ * Copy public folder contents to dist
857
507
  */
858
- _generateRouter(views) {
859
- let routeMap = '';
860
-
861
- views.forEach(v => {
862
- const routePath = this._generateRoutePath(v.file);
863
- const functionName = this._generateFunctionName(v.name);
864
- routeMap += ` '${routePath}': render${functionName},\n`;
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
- // --- JUX RUNTIME ERROR OVERLAY ---
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
- show: async function(error, title) {
942
- title = title || 'Runtime Error';
943
- var existing = document.getElementById('__jux-error-overlay');
944
- if (existing) existing.remove();
945
- await __juxLoadSources();
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, '&lt;').replace(/>/g, '&gt;') || ' ';
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 .jux and .js files preserving folder structure
529
+ * Recursively copy directory contents
1041
530
  */
1042
- _copySourceFilesRecursive(src, baseDir, distBase) {
531
+ _copyDirRecursive(src, dest, depth = 0) {
1043
532
  const entries = fs.readdirSync(src, { withFileTypes: true });
1044
533
 
1045
534
  entries.forEach(entry => {
1046
- if (entry.name.startsWith('.')) return; // Skip hidden
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 relativePath = path.relative(baseDir, srcPath);
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 if it doesn't exist
542
+ // Create directory and recurse
1054
543
  if (!fs.existsSync(destPath)) {
1055
544
  fs.mkdirSync(destPath, { recursive: true });
1056
545
  }
1057
- // Recurse into subdirectory
1058
- this._copySourceFilesRecursive(srcPath, baseDir, distBase);
1059
- } else if (entry.name.endsWith('.jux') || entry.name.endsWith('.js')) {
1060
- // Copy .jux and .js files (skip assets)
1061
- if (!this.isAssetFile(entry.name)) {
1062
- // Ensure parent directory exists
1063
- const destDir = path.dirname(destPath);
1064
- if (!fs.existsSync(destDir)) {
1065
- fs.mkdirSync(destDir, { recursive: true });
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
- * Copy ONLY public assets (CSS, images, fonts) to dist
1076
- * ❌ Does NOT copy .jux files (they are compiled separately)
562
+ * Get icon for file type
1077
563
  */
1078
- async copyPublicAssets() {
1079
- const { publicDir, distDir, paths } = this.config;
1080
-
1081
- // Resolve public folder path
1082
- const publicPath = paths?.public || path.resolve(this.config.srcDir, '..', publicDir);
1083
-
1084
- if (!fs.existsSync(publicPath)) {
1085
- console.log(`ℹ️ No public folder found at ${publicPath}, skipping asset copy`);
1086
- return;
1087
- }
1088
-
1089
- console.log(`📦 Copying public assets from ${publicPath}...`);
1090
-
1091
- // ✅ ONLY copy known asset file types
1092
- const assetTypes = [
1093
- '.css', '.scss', '.sass', '.less', // Stylesheets
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.1.117",
3
+ "version": "1.1.120",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "index.js",