juxscript 1.0.20 → 1.0.21
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/bin/cli.js +121 -72
- package/lib/components/alert.ts +143 -92
- package/lib/components/badge.ts +93 -94
- package/lib/components/base/BaseComponent.ts +397 -0
- package/lib/components/base/FormInput.ts +322 -0
- package/lib/components/button.ts +40 -131
- package/lib/components/card.ts +57 -79
- package/lib/components/charts/areachart.ts +315 -0
- package/lib/components/charts/barchart.ts +421 -0
- package/lib/components/charts/doughnutchart.ts +263 -0
- package/lib/components/charts/lib/BaseChart.ts +402 -0
- package/lib/components/{chart-types.ts → charts/lib/chart-types.ts} +1 -1
- package/lib/components/{chart-utils.ts → charts/lib/chart-utils.ts} +1 -1
- package/lib/components/{chart.ts → charts/lib/chart.ts} +3 -3
- package/lib/components/checkbox.ts +255 -204
- package/lib/components/code.ts +31 -78
- package/lib/components/container.ts +113 -130
- package/lib/components/data.ts +37 -5
- package/lib/components/datepicker.ts +180 -147
- package/lib/components/dialog.ts +218 -221
- package/lib/components/divider.ts +63 -87
- package/lib/components/docs-data.json +498 -2404
- package/lib/components/dropdown.ts +191 -236
- package/lib/components/element.ts +196 -145
- package/lib/components/fileupload.ts +253 -167
- package/lib/components/guard.ts +92 -0
- package/lib/components/heading.ts +31 -97
- package/lib/components/helpers.ts +13 -6
- package/lib/components/hero.ts +51 -114
- package/lib/components/icon.ts +33 -120
- package/lib/components/icons.ts +2 -1
- package/lib/components/include.ts +76 -3
- package/lib/components/input.ts +155 -407
- package/lib/components/kpicard.ts +16 -16
- package/lib/components/list.ts +358 -261
- package/lib/components/loading.ts +142 -211
- package/lib/components/menu.ts +63 -152
- package/lib/components/modal.ts +42 -129
- package/lib/components/nav.ts +79 -101
- package/lib/components/paragraph.ts +38 -102
- package/lib/components/progress.ts +108 -166
- package/lib/components/radio.ts +283 -234
- package/lib/components/script.ts +19 -87
- package/lib/components/select.ts +189 -199
- package/lib/components/sidebar.ts +110 -141
- package/lib/components/style.ts +19 -82
- package/lib/components/switch.ts +254 -183
- package/lib/components/table.ts +1078 -208
- package/lib/components/tabs.ts +42 -106
- package/lib/components/theme-toggle.ts +73 -165
- package/lib/components/tooltip.ts +85 -316
- package/lib/components/write.ts +108 -127
- package/lib/jux.ts +67 -41
- package/machinery/build.js +466 -0
- package/machinery/compiler.js +354 -105
- package/machinery/server.js +23 -100
- package/machinery/watcher.js +153 -130
- package/package.json +1 -1
- package/presets/base.css +1166 -0
- package/presets/notion.css +2 -1975
- package/lib/adapters/base-adapter.js +0 -35
- package/lib/adapters/index.js +0 -33
- package/lib/adapters/mysql-adapter.js +0 -65
- package/lib/adapters/postgres-adapter.js +0 -70
- package/lib/adapters/sqlite-adapter.js +0 -56
- package/lib/components/areachart.ts +0 -1128
- package/lib/components/areachartsmooth.ts +0 -1380
- package/lib/components/barchart.ts +0 -1322
- package/lib/components/doughnutchart.ts +0 -1259
- package/lib/components/footer.ts +0 -165
- package/lib/components/header.ts +0 -187
- package/lib/components/layout.ts +0 -239
- package/lib/components/main.ts +0 -137
- package/lib/layouts/default.jux +0 -8
- package/lib/layouts/figma.jux +0 -0
- /package/lib/{themes → components/charts/lib}/charts.js +0 -0
package/machinery/compiler.js
CHANGED
|
@@ -19,111 +19,6 @@ function generateImportMapScript() {
|
|
|
19
19
|
</script>`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
23
|
-
* Compile a .jux file to .js and .html
|
|
24
|
-
*
|
|
25
|
-
* @param {string} juxFilePath - Path to the .jux file
|
|
26
|
-
* @param {Object} options - Compilation options
|
|
27
|
-
* @param {string} options.distDir - Output directory
|
|
28
|
-
* @param {string} options.projectRoot - Project root directory
|
|
29
|
-
* @param {boolean} options.isServe - Whether serving for development
|
|
30
|
-
* @returns {Promise<{jsOutputPath: string, htmlOutputPath: string}>}
|
|
31
|
-
*/
|
|
32
|
-
export async function compileJuxFile(juxFilePath, options = {}) {
|
|
33
|
-
const { distDir, projectRoot, isServe = false } = options;
|
|
34
|
-
|
|
35
|
-
const relativePath = path.relative(projectRoot, juxFilePath);
|
|
36
|
-
const parsedPath = path.parse(relativePath);
|
|
37
|
-
|
|
38
|
-
// Output paths
|
|
39
|
-
const outputDir = path.join(distDir, parsedPath.dir);
|
|
40
|
-
const jsOutputPath = path.join(outputDir, `${parsedPath.name}.js`);
|
|
41
|
-
const htmlOutputPath = path.join(outputDir, `${parsedPath.name}.html`);
|
|
42
|
-
|
|
43
|
-
// Ensure output directory exists
|
|
44
|
-
if (!fs.existsSync(outputDir)) {
|
|
45
|
-
fs.mkdirSync(outputDir, { recursive: true });
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
console.log(`📝 Compiling: ${relativePath}`);
|
|
49
|
-
|
|
50
|
-
// Read the .jux file
|
|
51
|
-
const juxContent = fs.readFileSync(juxFilePath, 'utf-8');
|
|
52
|
-
|
|
53
|
-
// Calculate depth for relative paths
|
|
54
|
-
const depth = parsedPath.dir.split(path.sep).filter(p => p).length;
|
|
55
|
-
const libPath = depth === 0 ? './lib/jux.js' : '../'.repeat(depth) + 'lib/jux.js';
|
|
56
|
-
|
|
57
|
-
// Transform imports
|
|
58
|
-
let transformedContent = juxContent;
|
|
59
|
-
|
|
60
|
-
// Replace common import patterns with calculated path
|
|
61
|
-
transformedContent = transformedContent.replace(
|
|
62
|
-
/from\s+['"]\.\.?\/lib\/jux\.js['"]/g,
|
|
63
|
-
`from '${libPath}'`
|
|
64
|
-
);
|
|
65
|
-
|
|
66
|
-
// Only inject import if:
|
|
67
|
-
// 1. File is not empty (ignoring whitespace and comments)
|
|
68
|
-
// 2. File uses 'jux.' but has no import statement
|
|
69
|
-
const contentWithoutComments = transformedContent
|
|
70
|
-
.replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments
|
|
71
|
-
.replace(/\/\/.*/g, '') // Remove line comments
|
|
72
|
-
.trim();
|
|
73
|
-
|
|
74
|
-
const hasContent = contentWithoutComments.length > 0;
|
|
75
|
-
const usesJux = /\bjux\./g.test(contentWithoutComments);
|
|
76
|
-
const hasImport = /import\s+.*from/.test(transformedContent);
|
|
77
|
-
|
|
78
|
-
if (hasContent && usesJux && !hasImport) {
|
|
79
|
-
transformedContent = `import { jux } from '${libPath}';\n\n${transformedContent}`;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Write the transformed JS
|
|
83
|
-
fs.writeFileSync(jsOutputPath, transformedContent);
|
|
84
|
-
|
|
85
|
-
console.log(` ✓ JS: ${path.relative(projectRoot, jsOutputPath)}`);
|
|
86
|
-
|
|
87
|
-
// Generate HTML with import map and correct script path
|
|
88
|
-
const scriptPath = `./${parsedPath.name}.js`;
|
|
89
|
-
const importMapScript = generateImportMapScript();
|
|
90
|
-
|
|
91
|
-
const html = `<!DOCTYPE html>
|
|
92
|
-
<html lang="en">
|
|
93
|
-
<head>
|
|
94
|
-
<meta charset="UTF-8">
|
|
95
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
96
|
-
<title>${parsedPath.name}</title>
|
|
97
|
-
</head>
|
|
98
|
-
<body data-theme="">
|
|
99
|
-
<!-- App container -->
|
|
100
|
-
<div id="app" data-jux-page="${parsedPath.name}"></div>
|
|
101
|
-
|
|
102
|
-
${importMapScript}
|
|
103
|
-
<script type="module" src="${scriptPath}"></script>
|
|
104
|
-
${isServe ? `
|
|
105
|
-
<!-- Hot reload -->
|
|
106
|
-
<script type="module">
|
|
107
|
-
const ws = new WebSocket('ws://localhost:3001');
|
|
108
|
-
ws.onmessage = (msg) => {
|
|
109
|
-
const data = JSON.parse(msg.data);
|
|
110
|
-
if (data.type === 'reload') {
|
|
111
|
-
console.log('🔄 Reloading page...');
|
|
112
|
-
location.reload();
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
ws.onerror = () => console.warn('⚠️ WebSocket connection failed');
|
|
116
|
-
</script>
|
|
117
|
-
` : ''}
|
|
118
|
-
</body>
|
|
119
|
-
</html>`;
|
|
120
|
-
|
|
121
|
-
fs.writeFileSync(htmlOutputPath, html);
|
|
122
|
-
console.log(` ✓ HTML: ${path.relative(projectRoot, htmlOutputPath)}`);
|
|
123
|
-
|
|
124
|
-
return { jsOutputPath, htmlOutputPath };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
22
|
/**
|
|
128
23
|
* Copy and build the JUX library from TypeScript to JavaScript
|
|
129
24
|
*
|
|
@@ -406,3 +301,357 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
|
|
|
406
301
|
|
|
407
302
|
return fileList;
|
|
408
303
|
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Bundle all .jux files into a single router-based main.js
|
|
307
|
+
*
|
|
308
|
+
* @param {string} projectRoot - Source directory (jux/)
|
|
309
|
+
* @param {string} distDir - Destination directory (jux-dist/)
|
|
310
|
+
* @param {Object} options - Bundle options
|
|
311
|
+
* @param {string} options.routePrefix - Route prefix (e.g., '/experiments')
|
|
312
|
+
* @returns {Promise<string>} - Returns the generated filename (e.g., 'main.1234567890.js')
|
|
313
|
+
*/
|
|
314
|
+
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
315
|
+
const startTime = performance.now();
|
|
316
|
+
const { routePrefix = '' } = options;
|
|
317
|
+
|
|
318
|
+
console.log('🔀 Bundling .jux files into router...');
|
|
319
|
+
|
|
320
|
+
const juxFiles = findFiles(projectRoot, '.jux');
|
|
321
|
+
|
|
322
|
+
if (juxFiles.length === 0) {
|
|
323
|
+
console.log(' No .jux files found');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
console.log(` Found ${juxFiles.length} .jux file(s)`);
|
|
328
|
+
|
|
329
|
+
const fileTimings = [];
|
|
330
|
+
const views = [];
|
|
331
|
+
const routes = [];
|
|
332
|
+
const sharedModules = new Map(); // Track shared .jux modules
|
|
333
|
+
|
|
334
|
+
for (const juxFile of juxFiles) {
|
|
335
|
+
const fileStartTime = performance.now();
|
|
336
|
+
|
|
337
|
+
const relativePath = path.relative(projectRoot, juxFile);
|
|
338
|
+
const parsedPath = path.parse(relativePath);
|
|
339
|
+
|
|
340
|
+
const rawFunctionName = parsedPath.dir
|
|
341
|
+
? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
|
|
342
|
+
: parsedPath.name;
|
|
343
|
+
|
|
344
|
+
const cleanFunctionName = rawFunctionName
|
|
345
|
+
.replace(/[-_]/g, ' ')
|
|
346
|
+
.split(' ')
|
|
347
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
348
|
+
.join('');
|
|
349
|
+
|
|
350
|
+
const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
|
|
351
|
+
const cleanRoutePath = routePath.replace(/\/+/g, '/');
|
|
352
|
+
|
|
353
|
+
const juxContent = fs.readFileSync(juxFile, 'utf-8');
|
|
354
|
+
|
|
355
|
+
// Check if this file exports components (shared module)
|
|
356
|
+
const hasExports = /export\s+(const|let|function|class|{)/.test(juxContent);
|
|
357
|
+
|
|
358
|
+
if (hasExports) {
|
|
359
|
+
// Extract exports to shared modules section
|
|
360
|
+
sharedModules.set(relativePath, extractSharedModule(juxContent, rawFunctionName));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
|
|
364
|
+
|
|
365
|
+
views.push(viewFunction);
|
|
366
|
+
routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
|
|
367
|
+
|
|
368
|
+
const fileTime = performance.now() - fileStartTime;
|
|
369
|
+
fileTimings.push({ file: relativePath, time: fileTime });
|
|
370
|
+
|
|
371
|
+
const exportNote = hasExports ? ' [+exports]' : '';
|
|
372
|
+
console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ✅ Show slowest files if any took >50ms
|
|
376
|
+
const slowFiles = fileTimings.filter(f => f.time > 50).sort((a, b) => b.time - a.time);
|
|
377
|
+
if (slowFiles.length > 0) {
|
|
378
|
+
console.log(`\n ⚠️ Slowest files:`);
|
|
379
|
+
slowFiles.slice(0, 3).forEach(f => {
|
|
380
|
+
console.log(` ${f.file}: ${f.time.toFixed(0)}ms`);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const bundleStartTime = performance.now();
|
|
385
|
+
const routerCode = generateRouterBundle(views, routes, sharedModules);
|
|
386
|
+
const bundleGenTime = performance.now() - bundleStartTime;
|
|
387
|
+
|
|
388
|
+
// ✅ Use fixed filename (no timestamp)
|
|
389
|
+
const mainJsFilename = 'main.js';
|
|
390
|
+
const mainJsPath = path.join(distDir, mainJsFilename);
|
|
391
|
+
|
|
392
|
+
// Write to dist/main.js
|
|
393
|
+
const writeStartTime = performance.now();
|
|
394
|
+
fs.writeFileSync(mainJsPath, routerCode);
|
|
395
|
+
const writeTime = performance.now() - writeStartTime;
|
|
396
|
+
|
|
397
|
+
const totalTime = performance.now() - startTime;
|
|
398
|
+
|
|
399
|
+
console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
|
|
400
|
+
console.log(`\n 📊 Bundle Statistics:`);
|
|
401
|
+
console.log(` Files processed: ${juxFiles.length}`);
|
|
402
|
+
console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
|
|
403
|
+
console.log(` Code generation: ${bundleGenTime.toFixed(0)}ms`);
|
|
404
|
+
console.log(` File write: ${writeTime.toFixed(0)}ms`);
|
|
405
|
+
console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
|
|
406
|
+
console.log('✅ Router bundle complete\n');
|
|
407
|
+
|
|
408
|
+
// ✅ Return the filename so index.html can reference it
|
|
409
|
+
return mainJsFilename;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Extract shared module exports from a .jux file
|
|
414
|
+
*
|
|
415
|
+
* @param {string} juxContent - Original .jux file content
|
|
416
|
+
* @param {string} moduleName - Module identifier
|
|
417
|
+
* @returns {string} Shared module code
|
|
418
|
+
*/
|
|
419
|
+
function extractSharedModule(juxContent, moduleName) {
|
|
420
|
+
// Remove string literals temporarily to avoid false matches
|
|
421
|
+
const strings = [];
|
|
422
|
+
|
|
423
|
+
// Handle template literals, single quotes, and double quotes separately
|
|
424
|
+
let code = juxContent
|
|
425
|
+
.replace(/`(?:\\.|[^`\\])*`/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; })
|
|
426
|
+
.replace(/"(?:\\.|[^"\\])*"/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; })
|
|
427
|
+
.replace(/'(?:\\.|[^'\\])*'/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; });
|
|
428
|
+
|
|
429
|
+
// Remove ALL imports - including _dev-imports.js
|
|
430
|
+
code = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
431
|
+
code = code.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
432
|
+
code = code.replace(/import\s+__STRING_\d+__;?\s*/g, '');
|
|
433
|
+
|
|
434
|
+
// Convert exports to declarations (keep function keyword for functions)
|
|
435
|
+
code = code.replace(/export\s+const\s+/g, 'const ');
|
|
436
|
+
code = code.replace(/export\s+let\s+/g, 'let ');
|
|
437
|
+
code = code.replace(/export\s+function\s+/g, 'function ');
|
|
438
|
+
code = code.replace(/export\s+class\s+/g, 'class ');
|
|
439
|
+
code = code.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
|
|
440
|
+
|
|
441
|
+
// Restore string literals
|
|
442
|
+
code = code.replace(/__STRING_(\d+)__/g, (_, index) => strings[index]);
|
|
443
|
+
|
|
444
|
+
return code;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* Transform .jux file content into a view function
|
|
449
|
+
*
|
|
450
|
+
* @param {string} juxContent - Original .jux file content
|
|
451
|
+
* @param {string} functionName - Function name for the view
|
|
452
|
+
* @param {string} pageName - Page name for data attribute
|
|
453
|
+
* @param {string} relativePath - Relative path of the .jux file
|
|
454
|
+
* @param {Map} sharedModules - Map of shared module paths to their code
|
|
455
|
+
* @returns {string} View function code
|
|
456
|
+
*/
|
|
457
|
+
function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
|
|
458
|
+
// Remove string literals temporarily to avoid false matches
|
|
459
|
+
const strings = [];
|
|
460
|
+
|
|
461
|
+
// Handle template literals, single quotes, and double quotes separately
|
|
462
|
+
let code = juxContent
|
|
463
|
+
.replace(/`(?:\\.|[^`\\])*`/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; })
|
|
464
|
+
.replace(/"(?:\\.|[^"\\])*"/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; })
|
|
465
|
+
.replace(/'(?:\\.|[^'\\])*'/g, (match) => { strings.push(match); return `__STRING_${strings.length - 1}__`; });
|
|
466
|
+
|
|
467
|
+
// Remove ALL imports - now matching against placeholders
|
|
468
|
+
code = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
469
|
+
code = code.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
470
|
+
code = code.replace(/import\s+__STRING_\d+__;?\s*/g, '');
|
|
471
|
+
|
|
472
|
+
// Handle exports - convert named exports to const declarations
|
|
473
|
+
code = code.replace(/export\s+const\s+(\w+)\s*=/g, 'const $1 =');
|
|
474
|
+
code = code.replace(/export\s+let\s+(\w+)\s*=/g, 'let $1 =');
|
|
475
|
+
code = code.replace(/export\s+function\s+(\w+)/g, 'const $1 = function');
|
|
476
|
+
code = code.replace(/export\s+class\s+/g, 'class ');
|
|
477
|
+
code = code.replace(/export\s+default\s+/g, '');
|
|
478
|
+
code = code.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
|
|
479
|
+
|
|
480
|
+
// Only replace .renderTo(container) calls - leave explicit render targets alone
|
|
481
|
+
code = code.replace(/\.renderTo\(container\)/g, '.render("#app")');
|
|
482
|
+
|
|
483
|
+
// Only replace empty .render() calls (no arguments)
|
|
484
|
+
code = code.replace(/\.render\(\s*\)/g, '.render("#app")');
|
|
485
|
+
|
|
486
|
+
// Restore string literals
|
|
487
|
+
code = code.replace(/__STRING_(\d+)__/g, (_, index) => strings[index]);
|
|
488
|
+
|
|
489
|
+
const cleanName = functionName
|
|
490
|
+
.replace(/[-_]/g, ' ')
|
|
491
|
+
.split(' ')
|
|
492
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
493
|
+
.join('');
|
|
494
|
+
|
|
495
|
+
return `
|
|
496
|
+
// View: ${cleanName}
|
|
497
|
+
function ${cleanName}() {
|
|
498
|
+
${code}
|
|
499
|
+
|
|
500
|
+
return document.getElementById('app');
|
|
501
|
+
}`;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Generate complete router bundle with all views and routing logic
|
|
506
|
+
*
|
|
507
|
+
* @param {string[]} views - Array of view function code
|
|
508
|
+
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
509
|
+
* @param {Map} sharedModules - Map of shared module code
|
|
510
|
+
* @returns {string} Complete bundle code
|
|
511
|
+
*/
|
|
512
|
+
function generateRouterBundle(views, routes, sharedModules = new Map()) {
|
|
513
|
+
const libImport = './lib/jux.js';
|
|
514
|
+
|
|
515
|
+
// Generate shared modules section
|
|
516
|
+
const sharedModulesCode = Array.from(sharedModules.values())
|
|
517
|
+
.filter(code => code.trim())
|
|
518
|
+
.join('\n\n');
|
|
519
|
+
|
|
520
|
+
// Filter out empty views (shared modules)
|
|
521
|
+
const viewsCode = views.filter(v => v.trim()).join('\n\n');
|
|
522
|
+
|
|
523
|
+
const routeTable = routes
|
|
524
|
+
.map(r => ` '${r.path}': ${r.functionName}`)
|
|
525
|
+
.join(',\n');
|
|
526
|
+
|
|
527
|
+
return `// Generated Jux Router Bundle
|
|
528
|
+
import { jux, state } from '${libImport}';
|
|
529
|
+
|
|
530
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
531
|
+
// SHARED MODULES
|
|
532
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
533
|
+
|
|
534
|
+
${sharedModulesCode}
|
|
535
|
+
|
|
536
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
537
|
+
// VIEW FUNCTIONS
|
|
538
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
539
|
+
|
|
540
|
+
${viewsCode}
|
|
541
|
+
|
|
542
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
543
|
+
// 404 VIEW
|
|
544
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
545
|
+
|
|
546
|
+
function JuxNotFound() {
|
|
547
|
+
jux.heading(1, { text: '404 - Page Not Found' }).render('#app');
|
|
548
|
+
jux.paragraph('404-msg')
|
|
549
|
+
.text('The page you are looking for does not exist.')
|
|
550
|
+
.render('#app');
|
|
551
|
+
|
|
552
|
+
return document.getElementById('app');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
556
|
+
// 403 VIEW
|
|
557
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
558
|
+
|
|
559
|
+
function JuxForbidden() {
|
|
560
|
+
jux.heading(1, { text: '403 - Forbidden' }).render('#app');
|
|
561
|
+
jux.paragraph('403-msg')
|
|
562
|
+
.text('You are not authorized to view this page.')
|
|
563
|
+
.render('#app');
|
|
564
|
+
|
|
565
|
+
return document.getElementById('app');
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
569
|
+
// ROUTE TABLE
|
|
570
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
571
|
+
|
|
572
|
+
const routes = {
|
|
573
|
+
${routeTable}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
577
|
+
// ROUTER CORE
|
|
578
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
579
|
+
|
|
580
|
+
const app = document.getElementById('app');
|
|
581
|
+
|
|
582
|
+
function render() {
|
|
583
|
+
const path = location.pathname;
|
|
584
|
+
const view = routes[path] || JuxNotFound;
|
|
585
|
+
|
|
586
|
+
app.innerHTML = '';
|
|
587
|
+
app.removeAttribute('data-jux-page');
|
|
588
|
+
|
|
589
|
+
view();
|
|
590
|
+
|
|
591
|
+
const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
|
|
592
|
+
app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
document.addEventListener('click', e => {
|
|
596
|
+
const a = e.target.closest('a');
|
|
597
|
+
if (!a) return;
|
|
598
|
+
|
|
599
|
+
if (a.dataset.router === 'false') return;
|
|
600
|
+
|
|
601
|
+
const url = new URL(a.href);
|
|
602
|
+
if (url.origin !== location.origin) return;
|
|
603
|
+
|
|
604
|
+
e.preventDefault();
|
|
605
|
+
history.pushState({}, '', url.pathname);
|
|
606
|
+
render();
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
window.addEventListener('popstate', render);
|
|
610
|
+
|
|
611
|
+
render();
|
|
612
|
+
`;
|
|
613
|
+
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Generate a unified index.html for router bundle
|
|
618
|
+
*
|
|
619
|
+
* @param {string} distDir - Destination directory
|
|
620
|
+
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
621
|
+
* @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
|
|
622
|
+
*/
|
|
623
|
+
export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
624
|
+
console.log('📄 Generating index.html...');
|
|
625
|
+
|
|
626
|
+
// Generate navigation links
|
|
627
|
+
const navLinks = routes
|
|
628
|
+
.map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
|
|
629
|
+
.join(' |\n');
|
|
630
|
+
|
|
631
|
+
const importMapScript = generateImportMapScript();
|
|
632
|
+
|
|
633
|
+
const html = `<!DOCTYPE html>
|
|
634
|
+
<html lang="en">
|
|
635
|
+
<head>
|
|
636
|
+
<meta charset="UTF-8">
|
|
637
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
638
|
+
<title>Jux Application</title>
|
|
639
|
+
</head>
|
|
640
|
+
<body data-theme="">
|
|
641
|
+
<!-- Navigation -->
|
|
642
|
+
<nav style="padding: 20px; background: #f5f5f5; border-bottom: 2px solid #ddd;">
|
|
643
|
+
${navLinks}
|
|
644
|
+
</nav>
|
|
645
|
+
<!-- App container - router renders here -->
|
|
646
|
+
<div id="app"></div>
|
|
647
|
+
${importMapScript}
|
|
648
|
+
<script type="module" src="/${mainJsFilename}"></script>
|
|
649
|
+
</body>
|
|
650
|
+
</html>`;
|
|
651
|
+
|
|
652
|
+
const indexPath = path.join(distDir, 'index.html');
|
|
653
|
+
fs.writeFileSync(indexPath, html);
|
|
654
|
+
|
|
655
|
+
console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
|
|
656
|
+
console.log('✅ Index HTML complete\n');
|
|
657
|
+
}
|
package/machinery/server.js
CHANGED
|
@@ -3,15 +3,12 @@ import http from 'http';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
-
import initSqlJs from 'sql.js';
|
|
7
6
|
import { startWatcher } from './watcher.js';
|
|
8
7
|
import { WebSocketServer } from 'ws';
|
|
9
8
|
|
|
10
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
10
|
const __dirname = path.dirname(__filename);
|
|
12
11
|
|
|
13
|
-
let db = null;
|
|
14
|
-
|
|
15
12
|
async function serve(port = 3000, distDir = './jux-dist') {
|
|
16
13
|
const app = express();
|
|
17
14
|
const absoluteDistDir = path.resolve(distDir);
|
|
@@ -25,29 +22,16 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
25
22
|
process.exit(1);
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
//
|
|
29
|
-
app.
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (!db) {
|
|
34
|
-
return res.status(500).json({ error: 'Database not initialized' });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const stmt = db.prepare(sql);
|
|
38
|
-
stmt.bind(params);
|
|
25
|
+
// ✅ Strong cache prevention for main.js
|
|
26
|
+
app.use((req, res, next) => {
|
|
27
|
+
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0');
|
|
28
|
+
res.setHeader('Pragma', 'no-cache');
|
|
29
|
+
res.setHeader('Expires', '0');
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
rows.push(stmt.getAsObject());
|
|
43
|
-
}
|
|
44
|
-
stmt.free();
|
|
31
|
+
// ✅ Add ETag prevention
|
|
32
|
+
res.setHeader('ETag', 'W/"' + Date.now() + '"');
|
|
45
33
|
|
|
46
|
-
|
|
47
|
-
} catch (error) {
|
|
48
|
-
console.error('Query error:', error.message);
|
|
49
|
-
res.status(400).json({ error: error.message });
|
|
50
|
-
}
|
|
34
|
+
next();
|
|
51
35
|
});
|
|
52
36
|
|
|
53
37
|
// Disable caching in dev mode
|
|
@@ -58,74 +42,29 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
58
42
|
next();
|
|
59
43
|
});
|
|
60
44
|
|
|
61
|
-
// Serve HTML files with clean URLs
|
|
62
|
-
const heyPath = path.join(absoluteDistDir, 'hey.html');
|
|
63
|
-
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
64
|
-
|
|
65
|
-
app.use((req, res, next) => {
|
|
66
|
-
let requestPath = req.path.endsWith('/') && req.path.length > 1
|
|
67
|
-
? req.path.slice(0, -1)
|
|
68
|
-
: req.path;
|
|
69
|
-
|
|
70
|
-
// Root path - serve hey.html or index.html
|
|
71
|
-
if (requestPath === '/') {
|
|
72
|
-
if (fs.existsSync(heyPath)) {
|
|
73
|
-
return res.sendFile(heyPath);
|
|
74
|
-
}
|
|
75
|
-
if (fs.existsSync(indexPath)) {
|
|
76
|
-
return res.sendFile(indexPath);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Try to serve as HTML file
|
|
81
|
-
const htmlPath = path.join(absoluteDistDir, requestPath + '.html');
|
|
82
|
-
if (fs.existsSync(htmlPath) && fs.statSync(htmlPath).isFile()) {
|
|
83
|
-
return res.sendFile(htmlPath);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Try to serve index.html in directory
|
|
87
|
-
const indexInDirPath = path.join(absoluteDistDir, requestPath, 'index.html');
|
|
88
|
-
if (fs.existsSync(indexInDirPath)) {
|
|
89
|
-
return res.sendFile(indexInDirPath);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
next();
|
|
93
|
-
});
|
|
94
|
-
|
|
95
45
|
// Serve static files (CSS, JS, images, etc.)
|
|
96
46
|
app.use(express.static(absoluteDistDir));
|
|
97
47
|
|
|
98
|
-
//
|
|
99
|
-
app.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (fs.existsSync(notFoundPath) && req.path !== '/404') {
|
|
112
|
-
const debugUrl = `/404?path=${encodeURIComponent(req.path)}&type=${fileType}&from=${encodeURIComponent(req.get('referer') || 'direct')}`;
|
|
113
|
-
return res.redirect(debugUrl);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Serve custom 404 page
|
|
117
|
-
if (fs.existsSync(notFoundPath)) {
|
|
118
|
-
return res.status(404).sendFile(notFoundPath);
|
|
48
|
+
// SPA fallback: only for requests that accept HTML (not static files)
|
|
49
|
+
app.get('*', (req, res) => {
|
|
50
|
+
// If request accepts HTML, serve index.html
|
|
51
|
+
if (req.accepts('html')) {
|
|
52
|
+
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
53
|
+
if (fs.existsSync(indexPath)) {
|
|
54
|
+
res.sendFile(indexPath);
|
|
55
|
+
} else {
|
|
56
|
+
res.status(404).send('index.html not found. Run `npx jux bundle` first.');
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// For non-HTML requests (like .js, .css), return 404
|
|
60
|
+
res.status(404).send('Not found');
|
|
119
61
|
}
|
|
120
|
-
|
|
121
|
-
// Fallback: minimal 404 response
|
|
122
|
-
res.status(404).send('<h1>404 - Not Found</h1>');
|
|
123
62
|
});
|
|
124
63
|
|
|
125
|
-
// Create HTTP server
|
|
64
|
+
// Create HTTP server
|
|
126
65
|
const server = http.createServer(app);
|
|
127
66
|
|
|
128
|
-
// WebSocket server
|
|
67
|
+
// WebSocket server for hot reload
|
|
129
68
|
const wss = new WebSocketServer({ port: 3001 });
|
|
130
69
|
const clients = [];
|
|
131
70
|
|
|
@@ -165,7 +104,6 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
165
104
|
console.log('\n\n👋 Shutting down server...');
|
|
166
105
|
wss.close();
|
|
167
106
|
server.close();
|
|
168
|
-
if (db) db = null;
|
|
169
107
|
process.exit(0);
|
|
170
108
|
};
|
|
171
109
|
|
|
@@ -175,21 +113,6 @@ async function serve(port = 3000, distDir = './jux-dist') {
|
|
|
175
113
|
return { server };
|
|
176
114
|
}
|
|
177
115
|
|
|
178
|
-
async function initDatabase() {
|
|
179
|
-
const SQL = await initSqlJs();
|
|
180
|
-
const dbPath = path.join(__dirname, '../db/jux.db');
|
|
181
|
-
|
|
182
|
-
if (fs.existsSync(dbPath)) {
|
|
183
|
-
const buffer = fs.readFileSync(dbPath);
|
|
184
|
-
db = new SQL.Database(buffer);
|
|
185
|
-
console.log('📊 Database loaded:', dbPath);
|
|
186
|
-
} else {
|
|
187
|
-
db = new SQL.Database();
|
|
188
|
-
console.log('📊 Using in-memory database');
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
116
|
export async function start(port = 3000) {
|
|
193
|
-
await initDatabase();
|
|
194
117
|
return serve(port, './jux-dist');
|
|
195
118
|
}
|