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.
- package/README.md +32 -1
- package/bin/cli.js +13 -33
- package/docs/grid.png +0 -0
- package/juxconfig.example.js +24 -2
- package/lib/components/base/BaseComponent.ts +20 -0
- package/lib/components/chart.ts +231 -0
- package/lib/components/container.ts +76 -118
- package/lib/components/grid.ts +291 -0
- package/lib/components/input.ts +55 -1
- package/lib/jux.ts +10 -29
- package/lib/utils/fetch.ts +553 -0
- package/machinery/ast.js +347 -0
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/machinery/compiler.js +427 -151
- package/machinery/server.js +16 -2
- package/machinery/ts-shim.js +46 -0
- package/package.json +5 -1
- package/presets/default/all.jux +2 -2
- package/presets/default/layout.css +120 -0
- package/lib/components/charts/areachart.ts +0 -315
- package/lib/components/charts/barchart.ts +0 -421
- package/lib/components/charts/doughnutchart.ts +0 -263
- package/lib/components/charts/lib/BaseChart.ts +0 -389
- package/lib/components/charts/lib/chart-types.ts +0 -159
- package/lib/components/charts/lib/chart-utils.ts +0 -160
- package/lib/components/charts/lib/chart.ts +0 -707
- package/lib/components/charts/lib/charts.js +0 -126
- package/lib/components/docs-data.json +0 -2075
- package/lib/components/kpicard.ts +0 -640
package/machinery/compiler.js
CHANGED
|
@@ -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
|
-
|
|
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(); //
|
|
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
|
-
//
|
|
328
|
-
|
|
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
|
-
//
|
|
332
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
381
|
-
return
|
|
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
|
|
386
|
-
*
|
|
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
|
|
390
|
-
const
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
426
|
+
try {
|
|
427
|
+
const ast = acorn.parse(juxContent, {
|
|
428
|
+
ecmaVersion: 'latest',
|
|
429
|
+
sourceType: 'module'
|
|
430
|
+
});
|
|
409
431
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
453
|
+
// ✅ Namespace default export
|
|
454
|
+
const namespacedCode = namespaceExportedIdentifiers(cleaned, sourceFilePath);
|
|
418
455
|
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* @param {string}
|
|
437
|
-
* @
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
635
|
+
${result}
|
|
502
636
|
|
|
503
637
|
return document.getElementById('app');
|
|
504
638
|
}`;
|
|
505
639
|
}
|
|
506
640
|
|
|
507
641
|
/**
|
|
508
|
-
*
|
|
509
|
-
*
|
|
510
|
-
* @param {string
|
|
511
|
-
* @
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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,
|
|
642
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|