juxscript 1.0.67 → 1.0.68

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.
@@ -5,41 +5,22 @@ import * as acorn from 'acorn';
5
5
 
6
6
  /**
7
7
  * Copy and build the JUX library from TypeScript to JavaScript
8
- *
9
- * @param {string} projectRoot - Root directory containing lib/
10
- * @param {string} distDir - Destination directory for built files
11
8
  */
12
9
  export async function copyLibToOutput(projectRoot, distDir) {
13
- // Simplified lib path resolution
14
10
  const libSrc = path.resolve(projectRoot, '../lib');
15
-
16
11
  if (!fs.existsSync(libSrc)) {
17
12
  throw new Error(`lib/ directory not found at ${libSrc}`);
18
13
  }
19
-
20
14
  const libDest = path.join(distDir, 'lib');
21
15
 
22
- console.log('📦 Building TypeScript library...');
23
- console.log(` From: ${libSrc}`);
24
- console.log(` To: ${libDest}`);
25
-
26
16
  if (fs.existsSync(libDest)) {
27
17
  fs.rmSync(libDest, { recursive: true });
28
18
  }
29
-
30
19
  fs.mkdirSync(libDest, { recursive: true });
31
20
 
32
- // Find all TypeScript entry points
33
21
  const tsFiles = findFiles(libSrc, '.ts');
22
+ if (tsFiles.length === 0) return;
34
23
 
35
- if (tsFiles.length === 0) {
36
- console.warn('⚠️ No TypeScript files found in lib/');
37
- return;
38
- }
39
-
40
- console.log(` Found ${tsFiles.length} TypeScript files`);
41
-
42
- // Build all TypeScript files with esbuild
43
24
  try {
44
25
  await esbuild.build({
45
26
  entryPoints: tsFiles,
@@ -49,84 +30,40 @@ export async function copyLibToOutput(projectRoot, distDir) {
49
30
  outbase: libSrc,
50
31
  platform: 'browser',
51
32
  target: 'es2020',
52
- loader: {
53
- '.ts': 'ts'
54
- },
55
- logLevel: 'warning'
33
+ loader: { '.ts': 'ts' },
34
+ logLevel: 'error'
56
35
  });
57
-
58
- console.log(' ✓ TypeScript compiled to JavaScript');
59
-
60
- // Copy non-TS files (CSS, HTML, etc.)
61
- console.log(' Copying lib assets...');
62
36
  copyNonTsFiles(libSrc, libDest);
63
- console.log(' ✓ Lib assets copied');
64
-
65
37
  } catch (err) {
66
38
  console.error('❌ Failed to build TypeScript:', err.message);
67
39
  throw err;
68
40
  }
69
-
70
- console.log('✅ Library ready\n');
71
41
  }
72
42
 
73
43
  /**
74
44
  * Copy project assets (CSS, JS, images) from jux/ to dist/
75
- *
76
- * @param {string} projectRoot - Source directory (jux/)
77
- * @param {string} distDir - Destination directory (jux-dist/)
78
45
  */
79
46
  export async function copyProjectAssets(projectRoot, distDir) {
80
- console.log('📦 Copying project assets...');
81
-
82
- // Find all CSS and JS files in project root (excluding node_modules, dist, .git)
83
47
  const allFiles = [];
84
48
  findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
85
49
 
86
- console.log(` Found ${allFiles.length} asset file(s)`);
87
-
88
50
  for (const srcPath of allFiles) {
89
51
  const relativePath = path.relative(projectRoot, srcPath);
90
52
  const destPath = path.join(distDir, relativePath);
91
53
  const destDir = path.dirname(destPath);
92
-
93
- // Create destination directory if needed
94
- if (!fs.existsSync(destDir)) {
95
- fs.mkdirSync(destDir, { recursive: true });
96
- }
97
-
98
- // Copy file
54
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
99
55
  fs.copyFileSync(srcPath, destPath);
100
- console.log(` ✓ ${relativePath}`);
101
56
  }
102
-
103
- console.log('✅ Project assets copied\n');
104
57
  }
105
58
 
106
59
  /**
107
- * Transpile TypeScript files from jux/ to jux-dist/, preserving folder structure
108
- *
109
- * @param {string} srcDir - Source directory (jux/)
110
- * @param {string} destDir - Destination directory (jux-dist/)
111
- * @example
112
- * // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
113
- * await transpileProjectTypeScript('jux/', 'jux-dist/');
60
+ * Transpile TypeScript files from jux/ to jux-dist/
114
61
  */
115
62
  export async function transpileProjectTypeScript(srcDir, destDir) {
116
- console.log('🔷 Transpiling TypeScript files...');
117
-
118
- // Find all TypeScript files in the project
119
63
  const tsFiles = findFiles(srcDir, '.ts');
120
-
121
- if (tsFiles.length === 0) {
122
- console.log(' No TypeScript files found in project');
123
- return;
124
- }
125
-
126
- console.log(` Found ${tsFiles.length} TypeScript file(s)`);
64
+ if (tsFiles.length === 0) return;
127
65
 
128
66
  try {
129
- // Build all TypeScript files with esbuild
130
67
  await esbuild.build({
131
68
  entryPoints: tsFiles,
132
69
  bundle: false,
@@ -135,21 +72,9 @@ export async function transpileProjectTypeScript(srcDir, destDir) {
135
72
  outbase: srcDir,
136
73
  platform: 'browser',
137
74
  target: 'es2020',
138
- loader: {
139
- '.ts': 'ts'
140
- },
141
- logLevel: 'warning'
142
- });
143
-
144
- // Log each transpiled file
145
- tsFiles.forEach(tsFile => {
146
- const relativePath = path.relative(srcDir, tsFile);
147
- const jsPath = relativePath.replace(/\.ts$/, '.js');
148
- console.log(` ✓ ${relativePath} → ${jsPath}`);
75
+ loader: { '.ts': 'ts' },
76
+ logLevel: 'error'
149
77
  });
150
-
151
- console.log('✅ TypeScript transpiled\n');
152
-
153
78
  } catch (err) {
154
79
  console.error('❌ Failed to transpile TypeScript:', err.message);
155
80
  throw err;
@@ -157,45 +82,26 @@ export async function transpileProjectTypeScript(srcDir, destDir) {
157
82
  }
158
83
 
159
84
  /**
160
- * Copy presets folder from lib to dist (maintaining directory structure)
161
- *
162
- * @param {string} packageRoot - Source package root directory
163
- * @param {string} distDir - Destination directory
85
+ * Copy presets (No-op log silenced)
164
86
  */
165
87
  export async function copyPresetsToOutput(packageRoot, distDir) {
166
- // No-op - presets stay in node_modules only
167
- console.log('ℹ️ Presets available via import maps (not copied to dist)\n');
88
+ // No-op
168
89
  }
169
90
 
170
- /**
171
- * Recursively find files with a specific extension
172
- *
173
- * @param {string} dir - Directory to search
174
- * @param {string} extension - File extension (e.g., '.ts')
175
- * @param {string[]} fileList - Accumulator for found files
176
- * @returns {string[]} Array of file paths
177
- */
178
91
  function findFiles(dir, extension, fileList = []) {
179
92
  const files = fs.readdirSync(dir);
180
-
181
93
  files.forEach(file => {
182
94
  const filePath = path.join(dir, file);
183
95
  const stat = fs.statSync(filePath);
184
-
185
96
  if (stat.isDirectory()) {
186
97
  findFiles(filePath, extension, fileList);
187
98
  } else if (file.endsWith(extension)) {
188
99
  fileList.push(filePath);
189
100
  }
190
101
  });
191
-
192
102
  return fileList;
193
103
  }
194
104
 
195
- /**
196
- * Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
197
- * ✅ Skip layouts folder
198
- */
199
105
  function copyNonTsFiles(src, dest) {
200
106
  const entries = fs.readdirSync(src, { withFileTypes: true });
201
107
 
@@ -204,17 +110,17 @@ function copyNonTsFiles(src, dest) {
204
110
  const destPath = path.join(dest, entry.name);
205
111
 
206
112
  if (entry.isDirectory()) {
207
- // Skip layouts folder
208
- if (entry.name === 'layouts') {
209
- continue;
210
- }
211
-
212
- if (!fs.existsSync(destPath)) {
213
- fs.mkdirSync(destPath, { recursive: true });
214
- }
113
+ if (entry.name === 'layouts') continue;
114
+ if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
215
115
  copyNonTsFiles(srcPath, destPath);
216
116
  } else if (entry.isFile()) {
217
117
  const ext = path.extname(entry.name);
118
+ if (ext === '.js' || ext === '.map') {
119
+ let tsSibling = '';
120
+ if (ext === '.js') tsSibling = srcPath.replace(/\.js$/, '.ts');
121
+ else if (ext === '.map') tsSibling = srcPath.replace(/\.js\.map$/, '.ts');
122
+ if (tsSibling && fs.existsSync(tsSibling)) continue;
123
+ }
218
124
  if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
219
125
  fs.copyFileSync(srcPath, destPath);
220
126
  }
@@ -222,127 +128,62 @@ function copyNonTsFiles(src, dest) {
222
128
  }
223
129
  }
224
130
 
225
- /**
226
- * Find project files with specific extensions, excluding certain directories
227
- *
228
- * @param {string} dir - Directory to search
229
- * @param {string[]} extensions - File extensions to find
230
- * @param {string[]} fileList - Accumulator for found files
231
- * @param {string} rootDir - Root directory for relative paths
232
- * @param {string[]} excludeDirs - Directories to exclude
233
- * @returns {string[]} Array of file paths
234
- */
235
131
  function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
236
132
  if (!fs.existsSync(dir)) return fileList;
237
-
238
133
  const entries = fs.readdirSync(dir, { withFileTypes: true });
239
-
240
134
  for (const entry of entries) {
241
135
  const fullPath = path.join(dir, entry.name);
242
-
243
136
  if (entry.isDirectory()) {
244
- // Skip excluded directories
245
- if (excludeDirs.includes(entry.name)) {
246
- continue;
247
- }
137
+ if (excludeDirs.includes(entry.name)) continue;
248
138
  findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
249
139
  } else {
250
- // Check if file has one of the desired extensions
251
140
  const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
252
- if (hasExtension) {
253
- fileList.push(fullPath);
254
- }
141
+ if (hasExtension) fileList.push(fullPath);
255
142
  }
256
143
  }
257
-
258
144
  return fileList;
259
145
  }
260
146
 
261
147
  /**
262
148
  * Bundle all .jux files into a single router-based main.js
263
- * ✅ MODIFIED: Vendor dependencies locally + User Routes + Smart Import Merging
264
149
  */
265
150
  export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
266
- const startTime = performance.now();
267
151
  const { routePrefix = '', config } = options;
268
-
269
- console.log('🔀 Bundling .jux files into router...');
270
-
271
152
  const juxFiles = findFiles(projectRoot, '.jux');
272
153
 
273
154
  if (juxFiles.length === 0) {
274
- console.log(' No .jux files found');
275
- // ✅ FIX: Return empty result instead of undefined
276
- return {
277
- mainJsFilename: 'main.js',
278
- routes: [],
279
- external: new Set(),
280
- vendoredPaths: {}
281
- };
155
+ return { mainJsFilename: 'main.js', routes: [], external: new Set(), vendoredPaths: {} };
282
156
  }
283
157
 
284
158
  const pages = config?.pages || {};
285
-
286
- const fileTimings = [];
287
159
  const views = [];
288
160
  const routes = [];
289
- const sharedModules = new Map(); // ✅ Map<filePath, exportCode>
290
-
291
- // ✅ CHANGE: imports collection now stores context { code, filePath }
161
+ const sharedModules = new Map();
292
162
  const allImports = [];
293
- const externalModules = new Set(); // ✅ NEW: Track external modules directly
294
-
295
- // ✅ MAP for user config routing: Key = relative file path, Value = CleanFunctionName
163
+ const externalModules = new Set();
296
164
  const fileToFunction = new Map();
297
165
 
298
166
  for (const juxFile of juxFiles) {
299
- const fileStartTime = performance.now();
300
-
301
167
  const relativePath = path.relative(projectRoot, juxFile);
302
168
  const parsedPath = path.parse(relativePath);
169
+ const rawFunctionName = parsedPath.dir ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}` : parsedPath.name;
170
+ const cleanFunctionName = rawFunctionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
303
171
 
304
- const rawFunctionName = parsedPath.dir
305
- ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
306
- : parsedPath.name;
307
-
308
- const cleanFunctionName = rawFunctionName
309
- .replace(/[-_]/g, ' ')
310
- .split(' ')
311
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
312
- .join('');
313
-
314
- // ✅ Store mapping for Config Route Resolution
315
- // Store both 'path/file.jux' and normalized versions
316
172
  fileToFunction.set(relativePath, cleanFunctionName);
317
173
  fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
318
174
 
319
175
  const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
320
176
  const cleanRoutePath = routePath.replace(/\/+/g, '/');
321
-
322
177
  const juxContent = fs.readFileSync(juxFile, 'utf-8');
323
178
 
324
- // ✅ FIX: Extract imports AND detect external modules in one pass
325
179
  try {
326
- const ast = acorn.parse(juxContent, {
327
- ecmaVersion: 'latest',
328
- sourceType: 'module'
329
- });
330
-
180
+ const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
331
181
  ast.body.forEach(node => {
332
182
  if (node.type === 'ImportDeclaration') {
333
183
  const importStatement = juxContent.slice(node.start, node.end);
334
-
335
- // ✅ Store Object with context instead of raw string
336
- allImports.push({
337
- code: importStatement,
338
- filePath: relativePath // relative to projectRoot
339
- });
340
-
184
+ allImports.push({ code: importStatement, filePath: relativePath });
341
185
  const moduleName = node.source.value;
342
- if (!moduleName.startsWith('.') &&
343
- !moduleName.startsWith('/') &&
344
- !moduleName.startsWith('http') &&
345
- !moduleName.startsWith('juxscript')) {
186
+ if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
346
187
  externalModules.add(moduleName);
347
188
  }
348
189
  }
@@ -351,69 +192,37 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
351
192
  console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
352
193
  }
353
194
 
354
- // Check if file has exports
355
195
  const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(juxContent);
356
-
357
196
  if (hasExports) {
358
- // ✅ Store exports with UNIQUE KEY (file path)
359
197
  const exportKey = relativePath;
360
198
  const exportCode = extractSharedModule(juxContent, rawFunctionName, relativePath);
361
-
362
- if (exportCode.trim()) {
363
- sharedModules.set(exportKey, exportCode);
364
- }
199
+ if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
365
200
  }
366
201
 
367
- // Transform to view function (exports removed entirely)
368
- const viewFunction = transformJuxToViewFunction(
369
- juxContent,
370
- rawFunctionName,
371
- parsedPath.name,
372
- relativePath,
373
- sharedModules
374
- );
375
-
202
+ const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
376
203
  views.push(viewFunction);
377
204
 
378
- // Auto-route based on filesystem
379
205
  if (config?.defaults?.autoRoute !== false) {
380
206
  routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
381
207
  }
382
-
383
- const fileTime = performance.now() - fileStartTime;
384
- fileTimings.push({ file: relativePath, time: fileTime });
385
-
386
- const exportNote = hasExports ? ' [+exports]' : '';
387
- // console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
388
208
  }
389
209
 
390
- // ✅ PROCESS USER CONFIGURED ROUTES
391
210
  if (pages) {
392
- console.log(' 🗺️ Mapping user-configured routes...');
393
-
394
211
  const resolveAndAddRoute = (urlPath, targetFile) => {
395
- // Clean up target file path (remove leading ./ and /)
396
212
  const cleanTarget = targetFile.replace(/^(\.\/|\/)/, '');
397
213
  const funcName = fileToFunction.get(cleanTarget);
398
-
399
214
  if (funcName) {
400
- // User routes take precedence (prepend to list)
401
215
  routes.unshift({ path: urlPath, functionName: funcName });
402
- console.log(` ➕ ${urlPath} → ${funcName}`);
403
216
  } else {
404
- console.warn(` ⚠️ Route target not found: ${targetFile} (Available keys: ${fileToFunction.size})`);
217
+ console.warn(` ⚠️ Route target not found: ${targetFile}`);
405
218
  }
406
219
  };
407
-
408
220
  Object.entries(pages).forEach(([key, value]) => {
409
221
  if (typeof value === 'string') {
410
- // Direct mapping: '/' -> './experiments/state.jux'
411
222
  resolveAndAddRoute(key, value);
412
223
  } else if (typeof value === 'object') {
413
- // Group mapping
414
224
  const prefix = value.prefix || '';
415
225
  const groupRoutes = value.routes || {};
416
-
417
226
  Object.entries(groupRoutes).forEach(([routePath, targetFile]) => {
418
227
  const fullPath = (prefix + routePath).replace(/\/+/g, '/');
419
228
  resolveAndAddRoute(fullPath, targetFile);
@@ -422,564 +231,199 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
422
231
  });
423
232
  }
424
233
 
425
- // ✅ Show slowest files if any took >50ms
426
- const slowFiles = fileTimings.filter(f => f.time > 50).sort((a, b) => b.time - a.time);
427
- if (slowFiles.length > 0) {
428
- console.log(`\n ⚠️ Slowest files:`);
429
- slowFiles.slice(0, 3).forEach(f => {
430
- console.log(` ${f.file}: ${f.time.toFixed(0)}ms`);
431
- });
432
- }
433
-
434
- const bundleStartTime = performance.now();
435
-
436
- // ✅ CHANGE: Single summary instead of verbose list
437
- if (externalModules.size > 0) {
438
- console.log(`\n 📦 External dependencies: ${Array.from(externalModules).join(', ')}`);
439
- }
440
-
441
234
  const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
442
-
443
235
  const routerCode = generateRouterBundle(views, routes, sharedModules, allImports, projectRoot);
444
- const bundleGenTime = performance.now() - bundleStartTime;
445
-
446
236
  const mainJsFilename = 'main.js';
447
237
  const mainJsPath = path.join(distDir, mainJsFilename);
448
-
449
- const writeStartTime = performance.now();
450
238
  fs.writeFileSync(mainJsPath, routerCode);
451
- const writeTime = performance.now() - writeStartTime;
452
-
453
- const totalTime = performance.now() - startTime;
454
-
455
- console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
456
- console.log(`\n 📊 Bundle Statistics:`);
457
- console.log(` Files processed: ${juxFiles.length}`);
458
- console.log(` External deps: ${externalModules.size}`);
459
- console.log(` Vendored locally: ${Object.keys(vendoredPaths).length}`);
460
- console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
461
- console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
462
- console.log('✅ Router bundle complete\n');
463
239
 
464
- // ✅ Return bundleResult with vendor info
465
240
  return {
466
241
  mainJsFilename,
467
242
  routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
468
- external: externalModules, // ✅ FIX: Use externalModules instead of external
243
+ external: externalModules,
469
244
  vendoredPaths
470
245
  };
471
246
  }
472
247
 
473
- /**
474
- * Extract shared module exports from a .jux file using AST parsing
475
- * ✅ Namespaces exported identifiers to prevent collisions
476
- */
477
248
  function extractSharedModule(juxContent, moduleName, sourceFilePath) {
478
249
  const exportLines = [];
479
-
480
250
  try {
481
- const ast = acorn.parse(juxContent, {
482
- ecmaVersion: 'latest',
483
- sourceType: 'module'
484
- });
485
-
251
+ const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
486
252
  ast.body.forEach(node => {
487
- if (node.type === 'ExportNamedDeclaration') {
488
- // Case 1: Inline export (export const foo = ...)
489
- if (node.declaration) {
490
- const exportCode = juxContent.slice(node.start, node.end);
491
- const cleaned = exportCode.replace(/^export\s+/, '');
492
-
493
- // ✅ Namespace the identifier
494
- const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
495
-
496
- exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
497
- }
498
- // Case 2: Named export block (export { foo, bar })
499
- else if (node.specifiers && node.specifiers.length > 0) {
500
- console.log(` ℹ️ Skipping named export block in ${sourceFilePath} (declarations already present)`);
501
- }
502
- }
503
- else if (node.type === 'ExportDefaultDeclaration') {
504
- const exportCode = juxContent.slice(node.start, node.end);
505
- const cleaned = exportCode.replace(/^export\s+default\s+/, '');
506
-
507
- // ✅ Namespace default export
508
- const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
509
-
510
- exportLines.push(`// From: ${sourceFilePath}\n${namespacedCode}`);
253
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
254
+ const exportCode = juxContent.slice(node.start, node.end).replace(/^export\s+/, '');
255
+ exportLines.push(`// From: ${sourceFilePath}\n${namespaceExportedIdentifiers(exportCode, sourceFilePath)}`);
256
+ } else if (node.type === 'ExportDefaultDeclaration') {
257
+ const exportCode = juxContent.slice(node.start, node.end).replace(/^export\s+default\s+/, '');
258
+ exportLines.push(`// From: ${sourceFilePath}\n${namespaceExportedIdentifiers(exportCode, sourceFilePath)}`);
511
259
  }
512
260
  });
513
-
514
261
  } catch (err) {
515
- console.error(`\n❌ Failed to parse ${sourceFilePath}`);
516
- console.error(` Syntax Error: ${err.message}`);
517
- console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
518
- throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}. Please fix and retry.`);
262
+ throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}.`);
519
263
  }
520
-
521
264
  return exportLines.join('\n\n');
522
265
  }
523
266
 
524
- /**
525
- * Namespace exported identifiers by appending source file path
526
- * @param {string} code - The export code (e.g., "function myFunction() { ... }")
527
- * @param {string} sourceFilePath - Source file path (e.g., "experiments/test1.jux")
528
- * @returns {string} - Namespaced code (e.g., "function myFunction$experiments$test1() { ... }")
529
- */
530
267
  function namespaceExportedIdentifiers(code, sourceFilePath) {
531
- // Create namespace suffix from file path
532
- const namespace = sourceFilePath
533
- .replace(/\.jux$/, '')
534
- .replace(/[\/\\]/g, '$')
535
- .replace(/[^a-zA-Z0-9$]/g, '_');
536
-
268
+ const namespace = sourceFilePath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
537
269
  try {
538
- const ast = acorn.parse(code, {
539
- ecmaVersion: 'latest',
540
- sourceType: 'module'
541
- });
542
-
543
- // Find all exported identifiers
270
+ const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
544
271
  const identifiers = [];
545
-
546
272
  ast.body.forEach(node => {
547
- if (node.type === 'FunctionDeclaration' && node.id) {
548
- identifiers.push({
549
- name: node.id.name,
550
- start: node.id.start,
551
- end: node.id.end
552
- });
553
- }
554
- else if (node.type === 'VariableDeclaration') {
555
- node.declarations.forEach(decl => {
556
- if (decl.id.type === 'Identifier') {
557
- identifiers.push({
558
- name: decl.id.name,
559
- start: decl.id.start,
560
- end: decl.id.end
561
- });
562
- }
563
- });
564
- }
565
- else if (node.type === 'ClassDeclaration' && node.id) {
566
- identifiers.push({
567
- name: node.id.name,
568
- start: node.id.start,
569
- end: node.id.end
570
- });
571
- }
273
+ if (node.type === 'FunctionDeclaration' && node.id) identifiers.push(node.id);
274
+ else if (node.type === 'VariableDeclaration') node.declarations.forEach(d => { if (d.id.type === 'Identifier') identifiers.push(d.id); });
275
+ else if (node.type === 'ClassDeclaration' && node.id) identifiers.push(node.id);
572
276
  });
573
-
574
- // Replace identifiers in reverse order (to preserve offsets)
575
277
  identifiers.sort((a, b) => b.start - a.start);
576
-
577
278
  let result = code;
578
279
  identifiers.forEach(id => {
579
- const namespacedName = `${id.name}$${namespace}`;
580
- result = result.slice(0, id.start) + namespacedName + result.slice(id.end);
280
+ result = result.slice(0, id.start) + `${id.name}$${namespace}` + result.slice(id.end);
581
281
  });
582
-
583
282
  return result;
584
-
585
283
  } catch (err) {
586
- console.warn(`⚠️ Failed to namespace exports in ${sourceFilePath}, returning as-is`);
587
284
  return code;
588
285
  }
589
286
  }
590
287
 
591
- /**
592
- * Transform .jux file content into a view function using AST
593
- * ✅ Rewrites imports to use namespaced identifiers
594
- */
595
288
  function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
596
289
  let result;
597
-
598
290
  try {
599
- const ast = acorn.parse(juxContent, {
600
- ecmaVersion: 'latest',
601
- sourceType: 'module'
602
- });
603
-
291
+ const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
604
292
  const nodesToRemove = [];
605
- const importReplacements = new Map(); // ✅ Track import → namespace mappings
606
-
293
+ const importReplacements = new Map();
607
294
  ast.body.forEach(node => {
608
- // Process import declarations
609
295
  if (node.type === 'ImportDeclaration') {
610
296
  const importPath = node.source.value;
611
-
612
- // Only process .jux imports
613
297
  if (importPath.endsWith('.jux')) {
614
- // Extract imported identifiers
615
298
  node.specifiers.forEach(spec => {
616
299
  if (spec.type === 'ImportSpecifier') {
617
- const localName = spec.local.name;
618
- const importedName = spec.imported.name;
619
-
620
- // Resolve import path to namespace
621
300
  const resolvedPath = resolveImportPath(importPath, relativePath);
622
- const namespace = resolvedPath
623
- .replace(/\.jux$/, '')
624
- .replace(/[\/\\]/g, '$')
625
- .replace(/[^a-zA-Z0-9$]/g, '_');
626
-
627
- const namespacedName = `${importedName}$${namespace}`;
628
-
629
- // Map local name to namespaced name
630
- importReplacements.set(localName, namespacedName);
301
+ const namespace = resolvedPath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
302
+ importReplacements.set(spec.local.name, `${spec.imported.name}$${namespace}`);
631
303
  }
632
304
  });
633
305
  }
634
-
635
- // ✅ CRITICAL FIX: Remove ALL imports so they don't break function scope
636
- // They are hoisted by generateRouterBundle
637
306
  nodesToRemove.push({ start: node.start, end: node.end });
638
307
  }
639
-
640
- // Remove export declarations
641
- if (node.type === 'ExportNamedDeclaration') {
642
- if (node.declaration) {
643
- nodesToRemove.push({ start: node.start, end: node.end });
644
- }
645
- else if (node.specifiers && node.specifiers.length > 0) {
646
- nodesToRemove.push({ start: node.start, end: node.end });
647
- }
648
- }
649
-
650
- if (node.type === 'ExportDefaultDeclaration') {
308
+ if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
651
309
  nodesToRemove.push({ start: node.start, end: node.end });
652
310
  }
653
311
  });
654
-
655
312
  nodesToRemove.sort((a, b) => b.start - a.start);
656
-
657
313
  result = juxContent;
658
- nodesToRemove.forEach(({ start, end }) => {
659
- result = result.slice(0, start) + result.slice(end);
660
- });
661
-
662
- result = result.trim();
663
-
664
- // ✅ Replace imported identifiers with namespaced versions
314
+ nodesToRemove.forEach(({ start, end }) => { result = result.slice(0, start) + result.slice(end); });
665
315
  importReplacements.forEach((namespacedName, localName) => {
666
- // Use regex to replace all occurrences of the identifier
667
- // Must use word boundaries to avoid partial matches
668
- const regex = new RegExp(`\\b${localName}\\b`, 'g');
669
- result = result.replace(regex, namespacedName);
316
+ result = result.replace(new RegExp(`\\b${localName}\\b`, 'g'), namespacedName);
670
317
  });
671
-
672
318
  } catch (err) {
673
- console.error(`\n❌ Failed to parse ${relativePath}`);
674
- console.error(` Syntax Error: ${err.message}`);
675
- console.error(` Line: ${err.loc?.line}, Column: ${err.loc?.column}\n`);
676
- throw new Error(`Invalid JavaScript syntax in ${relativePath}. Please fix and retry.`);
319
+ throw new Error(`Invalid JavaScript syntax in ${relativePath}`);
677
320
  }
678
-
679
- result = result.replace(/\.renderTo\(container\)/g, '.render("#app")');
680
- result = result.replace(/\.render\(\s*\)/g, '.render("#app")');
681
-
682
- const cleanName = functionName
683
- .replace(/[-_]/g, ' ')
684
- .split(' ')
685
- .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
686
- .join('');
687
-
688
- return `
689
- // View: ${cleanName}
690
- function ${cleanName}() {
691
- ${result}
692
-
693
- return document.getElementById('app');
694
- }`;
321
+ result = result.replace(/\.renderTo\(container\)/g, '.render("#app")').replace(/\.render\(\s*\)/g, '.render("#app")');
322
+ const cleanName = functionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
323
+ return `\n// View: ${cleanName}\nfunction ${cleanName}() {\n${result}\n \n return document.getElementById('app');\n}`;
695
324
  }
696
325
 
697
- /**
698
- * Resolve relative import path to absolute path
699
- * @param {string} importPath - Import path from import statement (e.g., './test1.jux')
700
- * @param {string} currentFilePath - Current file path (e.g., 'experiments/test3.jux')
701
- * @returns {string} - Resolved path (e.g., 'experiments/test1.jux')
702
- */
703
326
  function resolveImportPath(importPath, currentFilePath) {
704
327
  const currentDir = path.dirname(currentFilePath);
705
- const resolved = path.join(currentDir, importPath);
706
- return resolved.replace(/\\/g, '/'); // Normalize to forward slashes
328
+ return path.join(currentDir, importPath).replace(/\\/g, '/');
707
329
  }
708
330
 
709
- /**
710
- * Generate complete router bundle
711
- * ✅ IMPROVED: Context-Aware Import Resolution (resolves ../paths before dedupe)
712
- */
713
331
  function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = [], projectRoot = '') {
714
- const libImport = '/lib/jux.js'; // ✅ Use absolute path for core lib
715
-
716
- // Map<NormalizedSource, { defaults: Set<string>, named: Set<string>, namespace: string|null }>
717
332
  const mergedImports = new Map();
718
-
719
- // Helper to resolve relative paths to a canonical string
720
333
  const getCanonicalSource = (source, contextFilePath) => {
721
- // 1. Keep externals/aliases/css/http as-is
722
- if (!source.startsWith('.')) return source;
723
- if (source.includes('.jux')) return source; // Skipped later anyway
724
-
725
- // 2. Resolve relative paths against the file they came from
726
- // contextFilePath is relative to projectRoot (e.g. 'experiments/list.jux')
334
+ if (!source.startsWith('.') || source.includes('.jux')) return source;
727
335
  const dir = path.dirname(contextFilePath);
728
-
729
- // joinedPath is relative to projectRoot (e.g. '../lib/componentsv2')
730
336
  const joinedPath = path.join(dir, source);
731
-
732
- // 3. Normalize to alias if it points to lib
733
- // Matches 'lib/componentsv2' or '../lib/componentsv2' variants
734
- if (joinedPath.includes('lib/componentsv2')) {
735
- return 'juxscript/componentsv2';
736
- }
737
- if (joinedPath.includes('lib/components')) {
738
- return 'juxscript/components';
337
+ const v2Index = joinedPath.indexOf('lib/componentsv2');
338
+ if (v2Index !== -1) {
339
+ const subPath = joinedPath.substring(v2Index + 'lib/componentsv2'.length);
340
+ if (!subPath || subPath === '/index.js' || subPath === '/index') return 'juxscript';
341
+ return 'juxscript' + subPath.replace(/\\/g, '/');
739
342
  }
740
-
741
- // 4. Fallback: return as-is (but normalized separators)
343
+ if (joinedPath.includes('lib/components')) return 'juxscript/components';
742
344
  return joinedPath.replace(/\\/g, '/');
743
345
  };
744
346
 
745
347
  const importList = Array.isArray(allImports) ? allImports : Array.from(allImports);
746
-
747
348
  for (const item of importList) {
748
- const code = typeof item === 'string' ? item : item.code;
749
- const filePath = typeof item === 'string' ? '' : item.filePath; // Context file path
750
-
751
349
  try {
350
+ const code = typeof item === 'string' ? item : item.code;
351
+ const filePath = typeof item === 'string' ? '' : item.filePath;
752
352
  const ast = acorn.parse(code.trim(), { ecmaVersion: 'latest', sourceType: 'module' });
753
353
  const node = ast.body[0];
754
-
755
354
  if (node && node.type === 'ImportDeclaration') {
756
355
  const rawSource = node.source.value;
757
-
758
- // Skip base libs handled manually
759
- if (rawSource === 'juxscript' || rawSource === 'juxscript/reactivity') continue;
760
356
  if (rawSource.endsWith('.jux')) continue;
761
-
762
- // ✅ RESOLVE SOURCE USING CONTEXT (Fixes ./lib/.. vs ../../lib/..)
763
357
  const source = getCanonicalSource(rawSource, filePath);
764
-
765
- if (!mergedImports.has(source)) {
766
- mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
767
- }
358
+ if (!mergedImports.has(source)) mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
768
359
  const storage = mergedImports.get(source);
769
-
770
360
  node.specifiers.forEach(spec => {
771
- if (spec.type === 'ImportDefaultSpecifier') {
772
- storage.defaults.add(spec.local.name);
773
- } else if (spec.type === 'ImportSpecifier') {
774
- if (spec.imported.name !== spec.local.name) {
775
- storage.named.add(`${spec.imported.name} as ${spec.local.name}`);
776
- } else {
777
- storage.named.add(spec.imported.name);
778
- }
779
- } else if (spec.type === 'ImportNamespaceSpecifier') {
780
- storage.namespace = spec.local.name;
781
- }
361
+ if (spec.type === 'ImportDefaultSpecifier') storage.defaults.add(spec.local.name);
362
+ else if (spec.type === 'ImportSpecifier') storage.named.add(spec.imported.name !== spec.local.name ? `${spec.imported.name} as ${spec.local.name}` : spec.imported.name);
363
+ else if (spec.type === 'ImportNamespaceSpecifier') storage.namespace = spec.local.name;
782
364
  });
783
365
  }
784
- } catch (e) {
785
- console.warn('⚠️ Failed to parse collected import:', code);
786
- }
366
+ } catch (e) { }
787
367
  }
788
368
 
789
- // Generate cleaned export list
790
369
  const filteredImports = [];
791
-
792
370
  mergedImports.forEach((storage, source) => {
793
- // Namespace imports usually stand alone
794
- if (storage.namespace) {
795
- filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
796
- }
797
-
371
+ if (storage.namespace) filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
798
372
  const parts = [];
799
- if (storage.defaults.size > 0) {
800
- const arr = Array.from(storage.defaults);
801
- parts.push(arr[0]);
802
- }
803
-
804
- if (storage.named.size > 0) {
805
- parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
806
- }
807
-
808
- if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) {
809
- filteredImports.push(`import '${source}';`);
810
- } else if (parts.length > 0) {
811
- filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
812
- }
373
+ if (storage.defaults.size > 0) parts.push(Array.from(storage.defaults)[0]);
374
+ if (storage.named.size > 0) parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
375
+ if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) filteredImports.push(`import '${source}';`);
376
+ else if (parts.length > 0) filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
813
377
  });
814
378
 
815
- // Generate shared modules section (deduplicated by file)
816
- const sharedModulesCode = Array.from(sharedModules.values())
817
- .filter(code => code.trim())
818
- .join('\n\n');
819
-
820
- // Filter out empty views
821
- const viewsCode = views.filter(v => v.trim()).join('\n\n');
822
-
823
- const routeTable = routes
824
- .map(r => ` '${r.path}': ${r.functionName}`)
825
- .join(',\n');
826
-
827
- // Build imports section (now filtered)
828
- const importsSection = filteredImports.join('\n');
829
-
830
379
  return `// Generated Jux Router Bundle
831
- import { jux, state } from '${libImport}';
832
-
833
- // ═══════════════════════════════════════════════════════════════════
834
- // COLLECTED IMPORTS FROM SOURCE FILES
835
- // ═══════════════════════════════════════════════════════════════════
836
-
837
- ${importsSection}
838
-
839
- // ═══════════════════════════════════════════════════════════════════
840
- // SHARED MODULES (Exported Components)
841
- // ═══════════════════════════════════════════════════════════════════
380
+ ${filteredImports.join('\n')}
842
381
 
843
- ${sharedModulesCode}
382
+ // SHARED MODULES
383
+ ${Array.from(sharedModules.values()).filter(c => c.trim()).join('\n\n')}
844
384
 
845
- // ═══════════════════════════════════════════════════════════════════
846
- // VIEW FUNCTIONS
847
- // ═══════════════════════════════════════════════════════════════════
385
+ // VIEWS
386
+ ${views.filter(v => v.trim()).join('\n\n')}
848
387
 
849
- ${viewsCode}
850
-
851
- // ═══════════════════════════════════════════════════════════════════
852
- // 404 VIEW
853
- // ═══════════════════════════════════════════════════════════════════
854
-
855
- function JuxNotFound() {
856
- jux.heading(1, { text: '404 - Page Not Found' }).render('#app');
857
- jux.paragraph('404-msg')
858
- .text('The page you are looking for does not exist.')
859
- .render('#app');
860
-
861
- return document.getElementById('app');
862
- }
863
-
864
- // ═══════════════════════════════════════════════════════════════════
865
- // 403 VIEW
866
- // ═══════════════════════════════════════════════════════════════════
867
-
868
- function JuxForbidden() {
869
- jux.heading(1, { text: '403 - Forbidden' }).render('#app');
870
- jux.paragraph('403-msg')
871
- .text('You are not authorized to view this page.')
872
- .render('#app');
873
-
874
- return document.getElementById('app');
875
- }
876
-
877
- // ═══════════════════════════════════════════════════════════════════
878
- // ROUTE TABLE
879
- // ═══════════════════════════════════════════════════════════════════
388
+ function JuxNotFound() { jux.heading(1, { text: '404 - Page Not Found' }).render('#app'); return document.getElementById('app'); }
389
+ function JuxForbidden() { jux.heading(1, { text: '403 - Forbidden' }).render('#app'); return document.getElementById('app'); }
880
390
 
881
391
  const routes = {
882
- ${routeTable}
392
+ ${routes.map(r => ` '${r.path}': ${r.functionName}`).join(',\n')}
883
393
  };
884
394
 
885
- // ═══════════════════════════════════════════════════════════════════
886
- // ROUTER CORE
887
- // ═══════════════════════════════════════════════════════════════════
888
-
889
395
  const app = document.getElementById('app');
890
-
891
396
  function render() {
892
397
  let path = location.pathname;
893
-
894
- // Try exact match first
895
- let view = routes[path];
896
-
897
- // If no match and path ends with /, try appending 'index'
898
- if (!view && path.endsWith('/')) {
899
- view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
900
- }
901
-
902
- // If still no match and path doesn't end with /, try appending '/index'
903
- if (!view && !path.endsWith('/')) {
904
- view = routes[path + '/index'];
905
- }
906
-
907
- // Fall back to 404
398
+ let view = routes[path] || (path.endsWith('/') ? (routes[path + 'index'] || routes[path.slice(0, -1) + '/index']) : routes[path + '/index']);
908
399
  view = view || JuxNotFound;
909
-
910
400
  app.innerHTML = '';
911
401
  app.removeAttribute('data-jux-page');
912
-
913
402
  view();
914
-
915
403
  const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
916
404
  app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
917
405
  }
918
-
919
406
  document.addEventListener('click', e => {
920
407
  const a = e.target.closest('a');
921
- if (!a) return;
922
-
923
- if (a.dataset.router === 'false') return;
924
-
925
- const url = new URL(a.href);
926
- if (url.origin !== location.origin) return;
927
-
408
+ if (!a || a.dataset.router === 'false' || new URL(a.href).origin !== location.origin) return;
928
409
  e.preventDefault();
929
410
  history.pushState({}, '', url.pathname);
930
411
  render();
931
412
  });
932
-
933
413
  window.addEventListener('popstate', render);
934
-
935
414
  render();
936
415
  `;
937
-
938
416
  }
939
417
 
940
- /**
941
- * Generate a unified index.html for router bundle
942
- * ✅ MODIFIED: Use vendored paths instead of CDN + Hot Reload Script injection
943
- */
944
418
  export function generateIndexHtml(distDir, bundleResult, options = {}) {
945
- // ADD: Defensive check and fallback
946
- if (!bundleResult) {
947
- console.warn('⚠️ generateIndexHtml called without bundleResult, using defaults');
948
- bundleResult = {
949
- mainJsFilename: 'main.js',
950
- routes: [],
951
- vendoredPaths: {}
952
- };
953
- }
954
-
955
- const { mainJsFilename = 'main.js', routes = [], vendoredPaths = {} } = bundleResult;
419
+ const { mainJsFilename = 'main.js', vendoredPaths = {} } = bundleResult || {};
956
420
  const { isDev = false, wsPort = 3001 } = options;
957
-
958
- console.log('📄 Generating index.html...');
959
-
960
- // ✅ Use vendor import map
961
421
  const importMapScript = generateVendorImportMap(vendoredPaths);
962
-
963
- // ✅ Client-side Hot Reload Script
964
422
  const hotReloadScript = isDev ? `
965
423
  <script>
966
424
  (function() {
967
- console.log('🔌 JUX Hot Reload: Connecting to port ${wsPort}...');
968
425
  let ws = new WebSocket('ws://' + window.location.hostname + ':${wsPort}');
969
-
970
- ws.onopen = () => console.log('✅ JUX Hot Reload: Connected');
971
-
972
- ws.onmessage = (msg) => {
973
- try {
974
- const data = JSON.parse(msg.data);
975
- if (data.type === 'reload') {
976
- console.log('🔄 JUX Hot Reload: Refeshing...');
977
- window.location.reload();
978
- }
979
- } catch(e) { console.error('HMR Error:', e); }
980
- };
981
-
982
- ws.onclose = () => console.log('❌ JUX Hot Reload: Disconnected');
426
+ ws.onmessage = (msg) => { if (JSON.parse(msg.data).type === 'reload') window.location.reload(); };
983
427
  })();
984
428
  </script>` : '';
985
429
 
@@ -997,114 +441,34 @@ export function generateIndexHtml(distDir, bundleResult, options = {}) {
997
441
  ${hotReloadScript}
998
442
  </body>
999
443
  </html>`;
1000
-
1001
- const indexPath = path.join(distDir, 'index.html');
1002
- fs.writeFileSync(indexPath, html);
1003
-
1004
- console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
1005
- console.log('✅ Index HTML complete\n');
444
+ fs.writeFileSync(path.join(distDir, 'index.html'), html);
1006
445
  }
1007
446
 
1008
- /**
1009
- * ✅ NEW: Download and vendor external dependencies locally
1010
- * Copies dependencies from node_modules to .jux-dist/vendor/
1011
- * NO CDN - everything is local, zero hardcoded mappings
1012
- * BUNDLES dependencies to resolve internal imports
1013
- */
1014
447
  async function vendorExternalDependencies(externalDeps, distDir) {
1015
448
  const vendorDir = path.join(distDir, 'vendor');
1016
-
1017
- if (!fs.existsSync(vendorDir)) {
1018
- fs.mkdirSync(vendorDir, { recursive: true });
1019
- }
1020
-
449
+ if (!fs.existsSync(vendorDir)) fs.mkdirSync(vendorDir, { recursive: true });
1021
450
  const vendoredPaths = {};
1022
-
1023
451
  for (const dep of externalDeps) {
1024
452
  try {
1025
453
  const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
1026
-
1027
- if (!fs.existsSync(nodeModulePath)) {
1028
- console.warn(` ⚠️ ${dep} not found in node_modules`);
1029
- continue;
1030
- }
1031
-
1032
- const packageJsonPath = path.join(nodeModulePath, 'package.json');
1033
-
1034
- if (!fs.existsSync(packageJsonPath)) {
1035
- console.warn(` ⚠️ ${dep}: package.json not found`);
1036
- continue;
1037
- }
1038
-
1039
- const packageJson = JSON.parse(
1040
- fs.readFileSync(packageJsonPath, 'utf-8')
1041
- );
1042
-
1043
- const entryPoints = [
1044
- packageJson.module,
1045
- packageJson.browser,
1046
- packageJson.main,
1047
- 'index.js'
1048
- ].filter(Boolean);
1049
-
1050
- let entryFile = null;
1051
- for (const entry of entryPoints) {
1052
- const candidatePath = path.join(nodeModulePath, entry);
1053
- if (fs.existsSync(candidatePath)) {
1054
- entryFile = candidatePath;
1055
- break;
1056
- }
1057
- }
1058
-
1059
- if (!entryFile) {
1060
- console.warn(` ⚠️ ${dep}: no valid entry point`);
1061
- continue;
1062
- }
1063
-
1064
- const vendorFile = path.join(vendorDir, `${dep}.js`);
1065
-
1066
- await esbuild.build({
1067
- entryPoints: [entryFile],
1068
- bundle: true,
1069
- format: 'esm',
1070
- outfile: vendorFile,
1071
- platform: 'browser',
1072
- target: 'es2020',
1073
- minify: false,
1074
- logLevel: 'warning'
1075
- });
1076
-
454
+ if (!fs.existsSync(nodeModulePath)) continue;
455
+ const pkg = JSON.parse(fs.readFileSync(path.join(nodeModulePath, 'package.json'), 'utf-8'));
456
+ const entryPoints = [pkg.module, pkg.browser, pkg.main, 'index.js'].filter(Boolean);
457
+ let entryFile = entryPoints.map(e => path.join(nodeModulePath, e)).find(p => fs.existsSync(p));
458
+ if (!entryFile) continue;
459
+ await esbuild.build({ entryPoints: [entryFile], bundle: true, format: 'esm', outfile: path.join(vendorDir, `${dep}.js`), platform: 'browser', target: 'es2020', logLevel: 'silent' });
1077
460
  vendoredPaths[dep] = `/vendor/${dep}.js`;
1078
- console.log(` ✓ ${dep} → vendor/${dep}.js`);
1079
-
1080
- } catch (err) {
1081
- console.error(` ❌ ${dep}: ${err.message}`);
1082
- }
461
+ } catch (err) { }
1083
462
  }
1084
-
1085
463
  return vendoredPaths;
1086
464
  }
1087
465
 
1088
- /**
1089
- * ✅ NEW: Generate import map with local vendor paths
1090
- */
1091
466
  function generateVendorImportMap(vendoredPaths) {
1092
- // ✅ FIX: Use ABSOLUTE paths (start with /) to support nested routes (e.g. /app/dashboard)
1093
- // When using ./, paths are resolved relative to the current URL, which breaks on sub-pages.
1094
467
  const imports = {
1095
- "juxscript/componentsv2": "/lib/componentsv2/index.js", //
1096
- "juxscript/componentsv2/": "/lib/componentsv2/", //
1097
- ...vendoredPaths // ✅ These are LOCAL paths: "./vendor/axios.js"
468
+ "juxscript": "/lib/componentsv2/index.js",
469
+ "juxscript/": "/lib/componentsv2/",
470
+ ...vendoredPaths
1098
471
  };
1099
-
1100
- // Ensure all paths start with / to survive pushState routing
1101
- Object.keys(imports).forEach(key => {
1102
- if (imports[key].startsWith('./')) {
1103
- imports[key] = imports[key].substring(1);
1104
- }
1105
- });
1106
-
1107
- return `<script type="importmap">
1108
- ${JSON.stringify({ imports }, null, 2)}
1109
- </script>`;
472
+ Object.keys(imports).forEach(key => { if (imports[key].startsWith('./')) imports[key] = imports[key].substring(1); });
473
+ return `<script type="importmap">\n${JSON.stringify({ imports }, null, 2)}\n</script>`;
1110
474
  }