juxscript 1.0.59 → 1.0.61

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,23 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import esbuild from 'esbuild';
4
-
5
- /**
6
- * Generate import map script tag
7
- */
8
- function generateImportMapScript() {
9
- return `<script type="importmap">
10
- {
11
- "imports": {
12
- "juxscript": "./lib/jux.js",
13
- "juxscript/": "./lib/",
14
- "juxscript/reactivity": "./lib/reactivity/state.js",
15
- "juxscript/presets/": "./presets/",
16
- "juxscript/components/": "./lib/components/"
17
- }
18
- }
19
- </script>`;
20
- }
4
+ import * as acorn from 'acorn';
21
5
 
22
6
  /**
23
7
  * Copy and build the JUX library from TypeScript to JavaScript
@@ -224,7 +208,7 @@ function copyNonTsFiles(src, dest) {
224
208
  if (entry.name === 'layouts') {
225
209
  continue;
226
210
  }
227
-
211
+
228
212
  if (!fs.existsSync(destPath)) {
229
213
  fs.mkdirSync(destPath, { recursive: true });
230
214
  }
@@ -276,12 +260,7 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
276
260
 
277
261
  /**
278
262
  * Bundle all .jux files into a single router-based main.js
279
- *
280
- * @param {string} projectRoot - Source directory (jux/)
281
- * @param {string} distDir - Destination directory (jux-dist/)
282
- * @param {Object} options - Bundle options
283
- * @param {string} options.routePrefix - Route prefix (e.g., '/experiments')
284
- * @returns {Promise<string>} - Returns the generated filename (e.g., 'main.1234567890.js')
263
+ * ✅ MODIFIED: Vendor dependencies locally
285
264
  */
286
265
  export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
287
266
  const startTime = performance.now();
@@ -293,7 +272,13 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
293
272
 
294
273
  if (juxFiles.length === 0) {
295
274
  console.log(' No .jux files found');
296
- return;
275
+ // ✅ FIX: Return empty result instead of undefined
276
+ return {
277
+ mainJsFilename: 'main.js',
278
+ routes: [],
279
+ external: new Set(),
280
+ vendoredPaths: {}
281
+ };
297
282
  }
298
283
 
299
284
  console.log(` Found ${juxFiles.length} .jux file(s)`);
@@ -301,7 +286,9 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
301
286
  const fileTimings = [];
302
287
  const views = [];
303
288
  const routes = [];
304
- const sharedModules = new Map(); // Track shared .jux modules
289
+ const sharedModules = new Map(); // Map<filePath, exportCode>
290
+ const allImports = new Set();
291
+ const externalModules = new Set(); // ✅ NEW: Track external modules directly
305
292
 
306
293
  for (const juxFile of juxFiles) {
307
294
  const fileStartTime = performance.now();
@@ -324,15 +311,52 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
324
311
 
325
312
  const juxContent = fs.readFileSync(juxFile, 'utf-8');
326
313
 
327
- // Check if this file exports components (shared module)
328
- const hasExports = /export\s+(const|let|function|class|{)/.test(juxContent);
314
+ // FIX: Extract imports AND detect external modules in one pass
315
+ try {
316
+ const ast = acorn.parse(juxContent, {
317
+ ecmaVersion: 'latest',
318
+ sourceType: 'module'
319
+ });
320
+
321
+ ast.body.forEach(node => {
322
+ if (node.type === 'ImportDeclaration') {
323
+ const importStatement = juxContent.slice(node.start, node.end);
324
+ allImports.add(importStatement);
325
+
326
+ const moduleName = node.source.value;
327
+ if (!moduleName.startsWith('.') &&
328
+ !moduleName.startsWith('/') &&
329
+ !moduleName.startsWith('http') &&
330
+ !moduleName.startsWith('juxscript')) {
331
+ externalModules.add(moduleName);
332
+ }
333
+ }
334
+ });
335
+ } catch (parseErr) {
336
+ console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
337
+ }
338
+
339
+ // Check if file has exports
340
+ const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(juxContent);
329
341
 
330
342
  if (hasExports) {
331
- // Extract exports to shared modules section
332
- sharedModules.set(relativePath, extractSharedModule(juxContent, rawFunctionName));
343
+ // Store exports with UNIQUE KEY (file path)
344
+ const exportKey = relativePath;
345
+ const exportCode = extractSharedModule(juxContent, rawFunctionName, relativePath);
346
+
347
+ if (exportCode.trim()) {
348
+ sharedModules.set(exportKey, exportCode);
349
+ }
333
350
  }
334
351
 
335
- const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
352
+ // Transform to view function (exports removed entirely)
353
+ const viewFunction = transformJuxToViewFunction(
354
+ juxContent,
355
+ rawFunctionName,
356
+ parsedPath.name,
357
+ relativePath,
358
+ sharedModules
359
+ );
336
360
 
337
361
  views.push(viewFunction);
338
362
  routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
@@ -354,14 +378,20 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
354
378
  }
355
379
 
356
380
  const bundleStartTime = performance.now();
357
- const routerCode = generateRouterBundle(views, routes, sharedModules);
381
+
382
+ // ✅ CHANGE: Single summary instead of verbose list
383
+ if (externalModules.size > 0) {
384
+ console.log(`\n 📦 External dependencies: ${Array.from(externalModules).join(', ')}`);
385
+ }
386
+
387
+ const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
388
+
389
+ const routerCode = generateRouterBundle(views, routes, sharedModules, allImports);
358
390
  const bundleGenTime = performance.now() - bundleStartTime;
359
391
 
360
- // ✅ Use fixed filename (no timestamp)
361
392
  const mainJsFilename = 'main.js';
362
393
  const mainJsPath = path.join(distDir, mainJsFilename);
363
394
 
364
- // Write to dist/main.js
365
395
  const writeStartTime = performance.now();
366
396
  fs.writeFileSync(mainJsPath, routerCode);
367
397
  const writeTime = performance.now() - writeStartTime;
@@ -371,124 +401,228 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
371
401
  console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
372
402
  console.log(`\n 📊 Bundle Statistics:`);
373
403
  console.log(` Files processed: ${juxFiles.length}`);
404
+ console.log(` External deps: ${externalModules.size}`);
405
+ console.log(` Vendored locally: ${Object.keys(vendoredPaths).length}`);
374
406
  console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
375
- console.log(` Code generation: ${bundleGenTime.toFixed(0)}ms`);
376
- console.log(` File write: ${writeTime.toFixed(0)}ms`);
377
407
  console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
378
408
  console.log('✅ Router bundle complete\n');
379
409
 
380
- // ✅ Return the filename so index.html can reference it
381
- return mainJsFilename;
410
+ // ✅ Return bundleResult with vendor info
411
+ return {
412
+ mainJsFilename,
413
+ routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
414
+ external: externalModules, // ✅ FIX: Use externalModules instead of external
415
+ vendoredPaths
416
+ };
382
417
  }
383
418
 
384
419
  /**
385
- * Extract and replace all string literals with placeholders
386
- * Handles: template literals WITHOUT interpolations, single quotes, double quotes
387
- * SKIPS: Template literals WITH ${} interpolations (those are dynamic code)
420
+ * Extract shared module exports from a .jux file using AST parsing
421
+ * Namespaces exported identifiers to prevent collisions
388
422
  */
389
- function extractStrings(code) {
390
- const strings = [];
391
- let result = code;
392
-
393
- // 1. Template literals WITHOUT interpolations (static strings only)
394
- // Match backticks that DON'T contain ${
395
- result = result.replace(/`([^`$\\]|\\[^$])*`/g, (match) => {
396
- // Double-check it doesn't contain ${
397
- if (!match.includes('${')) {
398
- strings.push(match);
399
- return `__STRING_${strings.length - 1}__`;
400
- }
401
- return match; // Leave dynamic templates alone
402
- });
423
+ function extractSharedModule(juxContent, moduleName, sourceFilePath) {
424
+ const exportLines = [];
403
425
 
404
- // 2. Double-quoted strings (with escaped quotes)
405
- result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
406
- strings.push(match);
407
- return `__STRING_${strings.length - 1}__`;
408
- });
426
+ try {
427
+ const ast = acorn.parse(juxContent, {
428
+ ecmaVersion: 'latest',
429
+ sourceType: 'module'
430
+ });
409
431
 
410
- // 3. Single-quoted strings (with escaped quotes)
411
- result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
412
- strings.push(match);
413
- return `__STRING_${strings.length - 1}__`;
414
- });
432
+ ast.body.forEach(node => {
433
+ if (node.type === 'ExportNamedDeclaration') {
434
+ // Case 1: Inline export (export const foo = ...)
435
+ if (node.declaration) {
436
+ const exportCode = juxContent.slice(node.start, node.end);
437
+ const cleaned = exportCode.replace(/^export\s+/, '');
438
+
439
+ // ✅ Namespace the identifier
440
+ const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
441
+
442
+ exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
443
+ }
444
+ // Case 2: Named export block (export { foo, bar })
445
+ else if (node.specifiers && node.specifiers.length > 0) {
446
+ console.log(` ℹ️ Skipping named export block in ${sourceFilePath} (declarations already present)`);
447
+ }
448
+ }
449
+ else if (node.type === 'ExportDefaultDeclaration') {
450
+ const exportCode = juxContent.slice(node.start, node.end);
451
+ const cleaned = exportCode.replace(/^export\s+default\s+/, '');
415
452
 
416
- return { code: result, strings };
417
- }
453
+ // Namespace default export
454
+ const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
418
455
 
419
- /**
420
- * Restore string placeholders back to original strings
421
- */
422
- function restoreStrings(code, strings) {
423
- return code.replace(/__STRING_(\d+)__/g, (match, index) => {
424
- const idx = parseInt(index, 10);
425
- if (idx >= 0 && idx < strings.length) {
426
- return strings[idx];
427
- }
428
- console.warn(`[Compiler] String placeholder ${match} not found (idx: ${idx}, available: ${strings.length})`);
429
- return match; // Leave as-is for debugging
430
- });
456
+ exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
457
+ }
458
+ });
459
+
460
+ } catch (err) {
461
+ console.error(`\n❌ Failed to parse ${sourceFilePath}`);
462
+ console.error(` Syntax Error: ${err.message}`);
463
+ console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
464
+ throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}. Please fix and retry.`);
465
+ }
466
+
467
+ return exportLines.join('\n\n');
431
468
  }
432
469
 
433
470
  /**
434
- * Extract shared module exports from a .jux file
435
- *
436
- * @param {string} juxContent - Original .jux file content
437
- * @param {string} moduleName - Module identifier
438
- * @returns {string} Shared module code
471
+ * Namespace exported identifiers by appending source file path
472
+ * @param {string} code - The export code (e.g., "function myFunction() { ... }")
473
+ * @param {string} sourceFilePath - Source file path (e.g., "experiments/test1.jux")
474
+ * @returns {string} - Namespaced code (e.g., "function myFunction$experiments$test1() { ... }")
439
475
  */
440
- function extractSharedModule(juxContent, moduleName) {
441
- const { code, strings } = extractStrings(juxContent);
442
-
443
- // Remove ALL imports
444
- let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
445
- result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
446
- result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
447
-
448
- // Convert exports to declarations
449
- result = result.replace(/export\s+const\s+/g, 'const ');
450
- result = result.replace(/export\s+let\s+/g, 'let ');
451
- result = result.replace(/export\s+function\s+/g, 'function ');
452
- result = result.replace(/export\s+class\s+/g, 'class ');
453
- result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
454
-
455
- // Restore strings
456
- return restoreStrings(result, strings);
476
+ function namespaceExportedIdentifiers(code, sourceFilePath) {
477
+ // Create namespace suffix from file path
478
+ const namespace = sourceFilePath
479
+ .replace(/\.jux$/, '')
480
+ .replace(/[\/\\]/g, '$')
481
+ .replace(/[^a-zA-Z0-9$]/g, '_');
482
+
483
+ try {
484
+ const ast = acorn.parse(code, {
485
+ ecmaVersion: 'latest',
486
+ sourceType: 'module'
487
+ });
488
+
489
+ // Find all exported identifiers
490
+ const identifiers = [];
491
+
492
+ ast.body.forEach(node => {
493
+ if (node.type === 'FunctionDeclaration' && node.id) {
494
+ identifiers.push({
495
+ name: node.id.name,
496
+ start: node.id.start,
497
+ end: node.id.end
498
+ });
499
+ }
500
+ else if (node.type === 'VariableDeclaration') {
501
+ node.declarations.forEach(decl => {
502
+ if (decl.id.type === 'Identifier') {
503
+ identifiers.push({
504
+ name: decl.id.name,
505
+ start: decl.id.start,
506
+ end: decl.id.end
507
+ });
508
+ }
509
+ });
510
+ }
511
+ else if (node.type === 'ClassDeclaration' && node.id) {
512
+ identifiers.push({
513
+ name: node.id.name,
514
+ start: node.id.start,
515
+ end: node.id.end
516
+ });
517
+ }
518
+ });
519
+
520
+ // Replace identifiers in reverse order (to preserve offsets)
521
+ identifiers.sort((a, b) => b.start - a.start);
522
+
523
+ let result = code;
524
+ identifiers.forEach(id => {
525
+ const namespacedName = `${id.name}$${namespace}`;
526
+ result = result.slice(0, id.start) + namespacedName + result.slice(id.end);
527
+ });
528
+
529
+ return result;
530
+
531
+ } catch (err) {
532
+ console.warn(`⚠️ Failed to namespace exports in ${sourceFilePath}, returning as-is`);
533
+ return code;
534
+ }
457
535
  }
458
536
 
459
537
  /**
460
- * Transform .jux file content into a view function
461
- *
462
- * @param {string} juxContent - Original .jux file content
463
- * @param {string} functionName - Function name for the view
464
- * @param {string} pageName - Page name for data attribute
465
- * @param {string} relativePath - Relative path of the .jux file
466
- * @param {Map} sharedModules - Map of shared module paths to their code
467
- * @returns {string} View function code
538
+ * Transform .jux file content into a view function using AST
539
+ * ✅ Rewrites imports to use namespaced identifiers
468
540
  */
469
541
  function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
470
- const { code, strings } = extractStrings(juxContent);
471
-
472
- // Remove ALL imports
473
- let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
474
- result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
475
- result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
476
-
477
- // Handle exports
478
- result = result.replace(/export\s+const\s+(\w+)\s*=/g, 'const $1 =');
479
- result = result.replace(/export\s+let\s+(\w+)\s*=/g, 'let $1 =');
480
- result = result.replace(/export\s+function\s+(\w+)/g, 'const $1 = function');
481
- result = result.replace(/export\s+class\s+/g, 'class ');
482
- result = result.replace(/export\s+default\s+/g, '');
483
- result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
484
-
485
- // Replace render patterns
542
+ let result;
543
+
544
+ try {
545
+ const ast = acorn.parse(juxContent, {
546
+ ecmaVersion: 'latest',
547
+ sourceType: 'module'
548
+ });
549
+
550
+ const nodesToRemove = [];
551
+ const importReplacements = new Map(); // Track import → namespace mappings
552
+
553
+ ast.body.forEach(node => {
554
+ // Process import declarations
555
+ if (node.type === 'ImportDeclaration') {
556
+ const importPath = node.source.value;
557
+
558
+ // Only process .jux imports
559
+ if (importPath.endsWith('.jux')) {
560
+ // Extract imported identifiers
561
+ node.specifiers.forEach(spec => {
562
+ if (spec.type === 'ImportSpecifier') {
563
+ const localName = spec.local.name;
564
+ const importedName = spec.imported.name;
565
+
566
+ // Resolve import path to namespace
567
+ const resolvedPath = resolveImportPath(importPath, relativePath);
568
+ const namespace = resolvedPath
569
+ .replace(/\.jux$/, '')
570
+ .replace(/[\/\\]/g, '$')
571
+ .replace(/[^a-zA-Z0-9$]/g, '_');
572
+
573
+ const namespacedName = `${importedName}$${namespace}`;
574
+
575
+ // Map local name to namespaced name
576
+ importReplacements.set(localName, namespacedName);
577
+ }
578
+ });
579
+ }
580
+
581
+ nodesToRemove.push({ start: node.start, end: node.end });
582
+ }
583
+
584
+ // Remove export declarations
585
+ if (node.type === 'ExportNamedDeclaration') {
586
+ if (node.declaration) {
587
+ nodesToRemove.push({ start: node.start, end: node.end });
588
+ }
589
+ else if (node.specifiers && node.specifiers.length > 0) {
590
+ nodesToRemove.push({ start: node.start, end: node.end });
591
+ }
592
+ }
593
+
594
+ if (node.type === 'ExportDefaultDeclaration') {
595
+ nodesToRemove.push({ start: node.start, end: node.end });
596
+ }
597
+ });
598
+
599
+ nodesToRemove.sort((a, b) => b.start - a.start);
600
+
601
+ result = juxContent;
602
+ nodesToRemove.forEach(({ start, end }) => {
603
+ result = result.slice(0, start) + result.slice(end);
604
+ });
605
+
606
+ result = result.trim();
607
+
608
+ // ✅ Replace imported identifiers with namespaced versions
609
+ importReplacements.forEach((namespacedName, localName) => {
610
+ // Use regex to replace all occurrences of the identifier
611
+ // Must use word boundaries to avoid partial matches
612
+ const regex = new RegExp(`\\b${localName}\\b`, 'g');
613
+ result = result.replace(regex, namespacedName);
614
+ });
615
+
616
+ } catch (err) {
617
+ console.error(`\n❌ Failed to parse ${relativePath}`);
618
+ console.error(` Syntax Error: ${err.message}`);
619
+ console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
620
+ throw new Error(`Invalid JavaScript syntax in ${relativePath}. Please fix and retry.`);
621
+ }
622
+
486
623
  result = result.replace(/\.renderTo\(container\)/g, '.render("#app")');
487
624
  result = result.replace(/\.render\(\s*\)/g, '.render("#app")');
488
625
 
489
- // Restore strings
490
- result = restoreStrings(result, strings);
491
-
492
626
  const cleanName = functionName
493
627
  .replace(/[-_]/g, ' ')
494
628
  .split(' ')
@@ -498,40 +632,80 @@ function transformJuxToViewFunction(juxContent, functionName, pageName, relative
498
632
  return `
499
633
  // View: ${cleanName}
500
634
  function ${cleanName}() {
501
- ${result}
635
+ ${result}
502
636
 
503
637
  return document.getElementById('app');
504
638
  }`;
505
639
  }
506
640
 
507
641
  /**
508
- * Generate complete router bundle with all views and routing logic
509
- *
510
- * @param {string[]} views - Array of view function code
511
- * @param {Array<{path: string, functionName: string}>} routes - Route definitions
512
- * @param {Map} sharedModules - Map of shared module code
513
- * @returns {string} Complete bundle code
642
+ * Resolve relative import path to absolute path
643
+ * @param {string} importPath - Import path from import statement (e.g., './test1.jux')
644
+ * @param {string} currentFilePath - Current file path (e.g., 'experiments/test3.jux')
645
+ * @returns {string} - Resolved path (e.g., 'experiments/test1.jux')
646
+ */
647
+ function resolveImportPath(importPath, currentFilePath) {
648
+ const currentDir = path.dirname(currentFilePath);
649
+ const resolved = path.join(currentDir, importPath);
650
+ return resolved.replace(/\\/g, '/'); // Normalize to forward slashes
651
+ }
652
+
653
+ /**
654
+ * Generate complete router bundle
514
655
  */
515
- function generateRouterBundle(views, routes, sharedModules = new Map()) {
656
+ function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = new Set()) {
516
657
  const libImport = './lib/jux.js';
517
658
 
518
- // Generate shared modules section
659
+ // Filter out redundant imports
660
+ const filteredImports = Array.from(allImports)
661
+ .filter(imp => {
662
+ const impStr = imp.trim();
663
+
664
+ // Remove imports from 'juxscript' (already imported)
665
+ if (impStr.includes("from 'juxscript'") || impStr.includes('from "juxscript"')) {
666
+ return false;
667
+ }
668
+
669
+ // Remove imports from 'juxscript/reactivity' (already available)
670
+ if (impStr.includes("from 'juxscript/reactivity'") || impStr.includes('from "juxscript/reactivity"')) {
671
+ return false;
672
+ }
673
+
674
+ // ✅ CRITICAL: Remove ANY import that references a .jux file
675
+ if (impStr.includes('.jux')) {
676
+ return false;
677
+ }
678
+
679
+ // Keep everything else (external libraries like axios, etc.)
680
+ return impStr.length > 0;
681
+ });
682
+
683
+ // ✅ Generate shared modules section (deduplicated by file)
519
684
  const sharedModulesCode = Array.from(sharedModules.values())
520
685
  .filter(code => code.trim())
521
686
  .join('\n\n');
522
687
 
523
- // Filter out empty views (shared modules)
688
+ // Filter out empty views
524
689
  const viewsCode = views.filter(v => v.trim()).join('\n\n');
525
690
 
526
691
  const routeTable = routes
527
692
  .map(r => ` '${r.path}': ${r.functionName}`)
528
693
  .join(',\n');
529
694
 
695
+ // Build imports section (now filtered)
696
+ const importsSection = filteredImports.join('\n');
697
+
530
698
  return `// Generated Jux Router Bundle
531
699
  import { jux, state } from '${libImport}';
532
700
 
533
701
  // ═══════════════════════════════════════════════════════════════════
534
- // SHARED MODULES
702
+ // COLLECTED IMPORTS FROM SOURCE FILES
703
+ // ═══════════════════════════════════════════════════════════════════
704
+
705
+ ${importsSection}
706
+
707
+ // ═══════════════════════════════════════════════════════════════════
708
+ // SHARED MODULES (Exported Components)
535
709
  // ═══════════════════════════════════════════════════════════════════
536
710
 
537
711
  ${sharedModulesCode}
@@ -633,20 +807,25 @@ render();
633
807
 
634
808
  /**
635
809
  * Generate a unified index.html for router bundle
636
- *
637
- * @param {string} distDir - Destination directory
638
- * @param {Array<{path: string, functionName: string}>} routes - Route definitions
639
- * @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
810
+ * ✅ MODIFIED: Use vendored paths instead of CDN
640
811
  */
641
- export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
642
- console.log('📄 Generating index.html...');
812
+ export function generateIndexHtml(distDir, bundleResult) {
813
+ // ADD: Defensive check and fallback
814
+ if (!bundleResult) {
815
+ console.warn('⚠️ generateIndexHtml called without bundleResult, using defaults');
816
+ bundleResult = {
817
+ mainJsFilename: 'main.js',
818
+ routes: [],
819
+ vendoredPaths: {}
820
+ };
821
+ }
643
822
 
644
- // Generate navigation links
645
- const navLinks = routes
646
- .map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
647
- .join(' |\n');
823
+ const { mainJsFilename = 'main.js', routes = [], vendoredPaths = {} } = bundleResult;
648
824
 
649
- const importMapScript = generateImportMapScript();
825
+ console.log('📄 Generating index.html...');
826
+
827
+ // ✅ Use vendor import map
828
+ const importMapScript = generateVendorImportMap(vendoredPaths);
650
829
 
651
830
  const html = `<!DOCTYPE html>
652
831
  <html lang="en">
@@ -656,7 +835,6 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
656
835
  <title>Jux Application</title>
657
836
  </head>
658
837
  <body data-theme="">
659
- <!-- App container - router renders here -->
660
838
  <div id="app"></div>
661
839
  ${importMapScript}
662
840
  <script type="module" src="/${mainJsFilename}"></script>
@@ -669,3 +847,101 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
669
847
  console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
670
848
  console.log('✅ Index HTML complete\n');
671
849
  }
850
+
851
+ /**
852
+ * ✅ NEW: Download and vendor external dependencies locally
853
+ * Copies dependencies from node_modules to .jux-dist/vendor/
854
+ * NO CDN - everything is local, zero hardcoded mappings
855
+ * BUNDLES dependencies to resolve internal imports
856
+ */
857
+ async function vendorExternalDependencies(externalDeps, distDir) {
858
+ const vendorDir = path.join(distDir, 'vendor');
859
+
860
+ if (!fs.existsSync(vendorDir)) {
861
+ fs.mkdirSync(vendorDir, { recursive: true });
862
+ }
863
+
864
+ const vendoredPaths = {};
865
+
866
+ for (const dep of externalDeps) {
867
+ try {
868
+ const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
869
+
870
+ if (!fs.existsSync(nodeModulePath)) {
871
+ console.warn(` ⚠️ ${dep} not found in node_modules`);
872
+ continue;
873
+ }
874
+
875
+ const packageJsonPath = path.join(nodeModulePath, 'package.json');
876
+
877
+ if (!fs.existsSync(packageJsonPath)) {
878
+ console.warn(` ⚠️ ${dep}: package.json not found`);
879
+ continue;
880
+ }
881
+
882
+ const packageJson = JSON.parse(
883
+ fs.readFileSync(packageJsonPath, 'utf-8')
884
+ );
885
+
886
+ const entryPoints = [
887
+ packageJson.module,
888
+ packageJson.browser,
889
+ packageJson.main,
890
+ 'index.js'
891
+ ].filter(Boolean);
892
+
893
+ let entryFile = null;
894
+ for (const entry of entryPoints) {
895
+ const candidatePath = path.join(nodeModulePath, entry);
896
+ if (fs.existsSync(candidatePath)) {
897
+ entryFile = candidatePath;
898
+ break;
899
+ }
900
+ }
901
+
902
+ if (!entryFile) {
903
+ console.warn(` ⚠️ ${dep}: no valid entry point`);
904
+ continue;
905
+ }
906
+
907
+ const vendorFile = path.join(vendorDir, `${dep}.js`);
908
+
909
+ await esbuild.build({
910
+ entryPoints: [entryFile],
911
+ bundle: true,
912
+ format: 'esm',
913
+ outfile: vendorFile,
914
+ platform: 'browser',
915
+ target: 'es2020',
916
+ minify: false,
917
+ logLevel: 'warning'
918
+ });
919
+
920
+ vendoredPaths[dep] = `/vendor/${dep}.js`;
921
+ console.log(` ✓ ${dep} → vendor/${dep}.js`);
922
+
923
+ } catch (err) {
924
+ console.error(` ❌ ${dep}: ${err.message}`);
925
+ }
926
+ }
927
+
928
+ return vendoredPaths;
929
+ }
930
+
931
+ /**
932
+ * ✅ NEW: Generate import map with local vendor paths
933
+ */
934
+ function generateVendorImportMap(vendoredPaths) {
935
+ const imports = {
936
+ "juxscript": "./lib/jux.js",
937
+ "juxscript/": "./lib/",
938
+ "juxscript/reactivity": "./lib/reactivity/state.js",
939
+ "juxscript/presets/": "./presets/",
940
+ "juxscript/components/": "./lib/components/",
941
+ ...vendoredPaths // ✅ These are LOCAL paths: "./vendor/axios.js"
942
+ };
943
+
944
+ return `<script type="importmap">
945
+ ${JSON.stringify({ imports }, null, 2)}
946
+ </script>`;
947
+ }