juxscript 1.0.49 ā 1.0.50
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 +97 -96
- package/juxconfig.example.js +2 -2
- package/machinery/config.js +709 -0
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -68,7 +68,7 @@ if (command === 'create') {
|
|
|
68
68
|
execSync('npx jux init', { stdio: 'inherit' });
|
|
69
69
|
|
|
70
70
|
console.log(`\nš Creating .gitignore...`);
|
|
71
|
-
fs.writeFileSync('.gitignore',
|
|
71
|
+
fs.writeFileSync('.gitignore', `.jux/\nnode_modules/\n.DS_Store\n.env\n*.log\n`); // ā
Changed jux-dist/ to .jux/
|
|
72
72
|
console.log(` ā .gitignore created`);
|
|
73
73
|
|
|
74
74
|
// ā
Copy juxconfig.example.js from the package
|
|
@@ -78,13 +78,16 @@ if (command === 'create') {
|
|
|
78
78
|
if (fs.existsSync(configExampleSrc)) {
|
|
79
79
|
// Copy as juxconfig.js (working config)
|
|
80
80
|
const configDest = path.join(process.cwd(), 'juxconfig.js');
|
|
81
|
-
fs.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
if (!fs.existsSync(configDest)) {
|
|
82
|
+
fs.copyFileSync(configExampleSrc, configDest);
|
|
83
|
+
console.log('+ Created juxconfig.js');
|
|
84
|
+
}
|
|
85
|
+
|
|
85
86
|
const configExampleDest = path.join(process.cwd(), 'juxconfig.example.js');
|
|
86
|
-
fs.
|
|
87
|
-
|
|
87
|
+
if (!fs.existsSync(configExampleDest)) {
|
|
88
|
+
fs.copyFileSync(configExampleSrc, configExampleDest);
|
|
89
|
+
console.log('+ Created juxconfig.example.js (reference)');
|
|
90
|
+
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
console.log(`
|
|
@@ -354,82 +357,73 @@ async function buildProject(isServe = false) {
|
|
|
354
357
|
process.exit(1);
|
|
355
358
|
}
|
|
356
359
|
|
|
357
|
-
// Create structure
|
|
360
|
+
// Create jux/ structure
|
|
358
361
|
fs.mkdirSync(juxDir, { recursive: true });
|
|
359
362
|
|
|
360
|
-
// Copy
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
+
// ā
Copy presets/default/ directly to jux/ (no presets subfolder!)
|
|
364
|
+
const defaultPresetSrc = path.join(PATHS.packageRoot, 'presets', 'default');
|
|
365
|
+
|
|
366
|
+
if (fs.existsSync(defaultPresetSrc)) {
|
|
367
|
+
console.log('š¦ Copying default preset boilerplate...');
|
|
368
|
+
|
|
369
|
+
const entries = fs.readdirSync(defaultPresetSrc, { withFileTypes: true });
|
|
370
|
+
let copiedCount = 0;
|
|
371
|
+
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const srcPath = path.join(defaultPresetSrc, entry.name);
|
|
374
|
+
const destPath = path.join(juxDir, entry.name);
|
|
363
375
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const heyJuxSrc = path.join(PATHS.packageRoot, 'presets', 'hey.jux');
|
|
370
|
-
if (fs.existsSync(heyJuxSrc)) {
|
|
371
|
-
fs.copyFileSync(heyJuxSrc, indexJuxDest);
|
|
372
|
-
console.log('+ Created jux/index.jux from hey.jux template');
|
|
373
|
-
} else {
|
|
374
|
-
console.warn('ā ļø No template found, creating basic file');
|
|
375
|
-
const basicContent = `import { jux } from 'juxscript';\n\njux.heading('welcome').text('Welcome to JUX').render('#app');`;
|
|
376
|
-
fs.writeFileSync(indexJuxDest, basicContent);
|
|
377
|
-
console.log('+ Created jux/index.jux');
|
|
376
|
+
if (entry.isFile()) {
|
|
377
|
+
fs.copyFileSync(srcPath, destPath);
|
|
378
|
+
console.log(`+ Created jux/${entry.name}`);
|
|
379
|
+
copiedCount++;
|
|
380
|
+
}
|
|
378
381
|
}
|
|
382
|
+
|
|
383
|
+
console.log(`ā
Copied ${copiedCount} boilerplate file(s) to jux/\n`);
|
|
379
384
|
}
|
|
380
385
|
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
|
|
386
|
+
// ā
Create a simple index.jux if none exists
|
|
387
|
+
const indexJuxDest = path.join(juxDir, 'index.jux');
|
|
388
|
+
if (!fs.existsSync(indexJuxDest)) {
|
|
389
|
+
const basicContent = `import { jux, state } from 'juxscript';
|
|
384
390
|
|
|
385
|
-
|
|
386
|
-
|
|
391
|
+
jux.hero('welcome', {
|
|
392
|
+
title: 'Welcome to JUX',
|
|
393
|
+
subtitle: 'Start building your app'
|
|
394
|
+
}).render('#app');
|
|
387
395
|
|
|
388
|
-
|
|
389
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
390
|
-
|
|
391
|
-
for (const entry of entries) {
|
|
392
|
-
const srcPath = path.join(src, entry.name);
|
|
393
|
-
const destPath = path.join(dest, entry.name);
|
|
394
|
-
|
|
395
|
-
// Skip jux.jux since we already copied it to index.jux
|
|
396
|
-
if (entry.isFile() && entry.name === 'jux.jux') {
|
|
397
|
-
continue;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
if (entry.isDirectory()) {
|
|
401
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
402
|
-
copyRecursive(srcPath, destPath);
|
|
403
|
-
} else if (entry.isFile()) {
|
|
404
|
-
fs.copyFileSync(srcPath, destPath);
|
|
405
|
-
const relativePath = path.relative(presetsSrc, srcPath);
|
|
406
|
-
console.log(`+ Copied preset: presets/${relativePath}`);
|
|
407
|
-
copiedCount++;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
396
|
+
const count = state(0);
|
|
411
397
|
|
|
412
|
-
|
|
413
|
-
|
|
398
|
+
jux.button('increment')
|
|
399
|
+
.label('Click me!')
|
|
400
|
+
.bind('click', () => count.value++)
|
|
401
|
+
.style('margin: 2rem 0;')
|
|
402
|
+
.render('#app');
|
|
414
403
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
404
|
+
jux.paragraph('counter')
|
|
405
|
+
.sync('text', count, val => \`Count: \${val}\`)
|
|
406
|
+
.render('#app');
|
|
407
|
+
`;
|
|
408
|
+
fs.writeFileSync(indexJuxDest, basicContent);
|
|
409
|
+
console.log('+ Created jux/index.jux');
|
|
418
410
|
}
|
|
419
411
|
|
|
420
412
|
// Create package.json if it doesn't exist
|
|
421
413
|
const pkgPath = path.join(PATHS.projectRoot, 'package.json');
|
|
422
414
|
if (!fs.existsSync(pkgPath)) {
|
|
415
|
+
const projectName = path.basename(PATHS.projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
416
|
+
|
|
423
417
|
const pkgContent = {
|
|
424
|
-
"name":
|
|
425
|
-
"version": "1.0
|
|
418
|
+
"name": projectName,
|
|
419
|
+
"version": "0.1.0",
|
|
426
420
|
"type": "module",
|
|
427
421
|
"scripts": {
|
|
428
|
-
"
|
|
429
|
-
"
|
|
422
|
+
"dev": "jux serve",
|
|
423
|
+
"build": "jux build"
|
|
430
424
|
},
|
|
431
425
|
"dependencies": {
|
|
432
|
-
"juxscript": "
|
|
426
|
+
"juxscript": "latest"
|
|
433
427
|
}
|
|
434
428
|
};
|
|
435
429
|
fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
|
|
@@ -438,21 +432,39 @@ async function buildProject(isServe = false) {
|
|
|
438
432
|
|
|
439
433
|
// Create .gitignore
|
|
440
434
|
const gitignorePath = path.join(PATHS.projectRoot, '.gitignore');
|
|
441
|
-
const gitignoreContent =
|
|
442
|
-
node_modules/
|
|
443
|
-
.DS_Store
|
|
444
|
-
`;
|
|
435
|
+
const gitignoreContent = `.jux/\nnode_modules/\n.DS_Store\n`; // ā
Changed
|
|
445
436
|
|
|
446
437
|
if (!fs.existsSync(gitignorePath)) {
|
|
447
438
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
448
439
|
console.log('+ Created .gitignore');
|
|
449
440
|
}
|
|
450
441
|
|
|
442
|
+
// ā
Copy juxconfig to root
|
|
443
|
+
const configExampleSrc = path.join(PATHS.packageRoot, 'juxconfig.example.js');
|
|
444
|
+
|
|
445
|
+
if (fs.existsSync(configExampleSrc)) {
|
|
446
|
+
const configDest = path.join(PATHS.projectRoot, 'juxconfig.js');
|
|
447
|
+
if (!fs.existsSync(configDest)) {
|
|
448
|
+
fs.copyFileSync(configExampleSrc, configDest);
|
|
449
|
+
console.log('+ Created juxconfig.js');
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const configExampleDest = path.join(PATHS.projectRoot, 'juxconfig.example.js');
|
|
453
|
+
if (!fs.existsSync(configExampleDest)) {
|
|
454
|
+
fs.copyFileSync(configExampleSrc, configExampleDest);
|
|
455
|
+
console.log('+ Created juxconfig.example.js (reference)');
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
451
459
|
console.log('\nā
JUX project initialized!\n');
|
|
460
|
+
console.log('Project structure:');
|
|
461
|
+
console.log(' jux/ # Your source files');
|
|
462
|
+
console.log(' .jux/ # Build output (git-ignored)');
|
|
463
|
+
console.log(' juxconfig.js # Configuration');
|
|
464
|
+
console.log(' package.json # Dependencies\n');
|
|
452
465
|
console.log('Next steps:');
|
|
453
|
-
console.log(' npm install
|
|
454
|
-
console.log('
|
|
455
|
-
console.log('Check out the docs: https://juxscript.com/docs\n');
|
|
466
|
+
console.log(' npm install # Install dependencies');
|
|
467
|
+
console.log(' npm run dev # Start dev server\n');
|
|
456
468
|
|
|
457
469
|
} else if (command === 'build') {
|
|
458
470
|
// ā
Always builds router bundle
|
|
@@ -474,39 +486,28 @@ node_modules/
|
|
|
474
486
|
JUX CLI - A JavaScript UX authorship platform
|
|
475
487
|
|
|
476
488
|
Usage:
|
|
477
|
-
npx jux
|
|
478
|
-
npx jux
|
|
489
|
+
npx jux create [name] Create a new JUX project
|
|
490
|
+
npx jux init Initialize JUX in current directory
|
|
491
|
+
npx jux build Build router bundle to ./.jux/
|
|
479
492
|
npx jux serve [http] [ws] Start dev server with hot reload
|
|
480
493
|
|
|
481
|
-
Arguments:
|
|
482
|
-
[http] HTTP server port (default: 3000)
|
|
483
|
-
[ws] WebSocket port (default: 3001)
|
|
484
|
-
|
|
485
494
|
Project Structure:
|
|
486
495
|
my-project/
|
|
487
|
-
āāā jux/
|
|
488
|
-
ā āāā
|
|
489
|
-
ā
|
|
490
|
-
|
|
491
|
-
āāā
|
|
496
|
+
āāā .jux/ # Build output (git-ignored, like .git)
|
|
497
|
+
ā āāā lib/
|
|
498
|
+
ā āāā main.js
|
|
499
|
+
ā āāā index.html
|
|
500
|
+
āāā jux/ # Your .jux source files
|
|
501
|
+
ā āāā index.jux
|
|
502
|
+
ā āāā layout.css
|
|
503
|
+
ā āāā layout.jux
|
|
504
|
+
āāā juxconfig.js # Configuration
|
|
492
505
|
āāā package.json
|
|
493
506
|
|
|
494
|
-
Import Style:
|
|
495
|
-
// In your project's .jux files
|
|
496
|
-
import { jux, state } from 'juxscript';
|
|
497
|
-
import 'juxscript/presets/notion.js';
|
|
498
|
-
|
|
499
|
-
Getting Started:
|
|
500
|
-
1. npx jux init # Create project structure
|
|
501
|
-
2. npm install # Install dependencies
|
|
502
|
-
3. npx jux serve # Dev server with hot reload
|
|
503
|
-
4. Serve jux-dist/ from your backend
|
|
504
|
-
|
|
505
507
|
Examples:
|
|
506
|
-
npx jux
|
|
507
|
-
npx jux serve
|
|
508
|
-
npx jux serve 8080
|
|
509
|
-
npx jux serve 8080 8081 # HTTP on 8080, WS on 8081
|
|
508
|
+
npx jux create my-app Create new project
|
|
509
|
+
npx jux serve Dev server (ports 3000/3001)
|
|
510
|
+
npx jux serve 8080 8081 Custom ports
|
|
510
511
|
`);
|
|
511
512
|
}
|
|
512
513
|
})();
|
package/juxconfig.example.js
CHANGED
|
@@ -21,8 +21,8 @@ export default {
|
|
|
21
21
|
// Source directory for .jux files (default: 'jux')
|
|
22
22
|
sourceDir: 'jux',
|
|
23
23
|
|
|
24
|
-
// Output directory for built files (default: 'jux
|
|
25
|
-
distDir: 'jux-dist',
|
|
24
|
+
// Output directory for built files (default: '.jux')
|
|
25
|
+
distDir: '.jux', // ā
Changed from 'jux-dist',
|
|
26
26
|
|
|
27
27
|
services: {
|
|
28
28
|
database: 'http://localhost:4000/api/db',
|
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
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
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Copy and build the JUX library from TypeScript to JavaScript
|
|
24
|
+
*
|
|
25
|
+
* @param {string} projectRoot - Root directory containing lib/
|
|
26
|
+
* @param {string} distDir - Destination directory for built files
|
|
27
|
+
*/
|
|
28
|
+
export async function copyLibToOutput(projectRoot, distDir) {
|
|
29
|
+
// Simplified lib path resolution
|
|
30
|
+
const libSrc = path.resolve(projectRoot, '../lib');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(libSrc)) {
|
|
33
|
+
throw new Error(`lib/ directory not found at ${libSrc}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const libDest = path.join(distDir, 'lib');
|
|
37
|
+
|
|
38
|
+
console.log('š¦ Building TypeScript library...');
|
|
39
|
+
console.log(` From: ${libSrc}`);
|
|
40
|
+
console.log(` To: ${libDest}`);
|
|
41
|
+
|
|
42
|
+
if (fs.existsSync(libDest)) {
|
|
43
|
+
fs.rmSync(libDest, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fs.mkdirSync(libDest, { recursive: true });
|
|
47
|
+
|
|
48
|
+
// Find all TypeScript entry points
|
|
49
|
+
const tsFiles = findFiles(libSrc, '.ts');
|
|
50
|
+
|
|
51
|
+
if (tsFiles.length === 0) {
|
|
52
|
+
console.warn('ā ļø No TypeScript files found in lib/');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
console.log(` Found ${tsFiles.length} TypeScript files`);
|
|
57
|
+
|
|
58
|
+
// Build all TypeScript files with esbuild
|
|
59
|
+
try {
|
|
60
|
+
await esbuild.build({
|
|
61
|
+
entryPoints: tsFiles,
|
|
62
|
+
bundle: false,
|
|
63
|
+
format: 'esm',
|
|
64
|
+
outdir: libDest,
|
|
65
|
+
outbase: libSrc,
|
|
66
|
+
platform: 'browser',
|
|
67
|
+
target: 'es2020',
|
|
68
|
+
loader: {
|
|
69
|
+
'.ts': 'ts'
|
|
70
|
+
},
|
|
71
|
+
logLevel: 'warning'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
console.log(' ā TypeScript compiled to JavaScript');
|
|
75
|
+
|
|
76
|
+
// Copy non-TS files (CSS, HTML, etc.)
|
|
77
|
+
console.log(' Copying lib assets...');
|
|
78
|
+
copyNonTsFiles(libSrc, libDest);
|
|
79
|
+
console.log(' ā Lib assets copied');
|
|
80
|
+
|
|
81
|
+
} catch (err) {
|
|
82
|
+
console.error('ā Failed to build TypeScript:', err.message);
|
|
83
|
+
throw err;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log('ā
Library ready\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Copy project assets (CSS, JS, images) from jux/ to dist/
|
|
91
|
+
*
|
|
92
|
+
* @param {string} projectRoot - Source directory (jux/)
|
|
93
|
+
* @param {string} distDir - Destination directory (jux-dist/)
|
|
94
|
+
*/
|
|
95
|
+
export async function copyProjectAssets(projectRoot, distDir) {
|
|
96
|
+
console.log('š¦ Copying project assets...');
|
|
97
|
+
|
|
98
|
+
// Find all CSS and JS files in project root (excluding node_modules, dist, .git)
|
|
99
|
+
const allFiles = [];
|
|
100
|
+
findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
|
|
101
|
+
|
|
102
|
+
console.log(` Found ${allFiles.length} asset file(s)`);
|
|
103
|
+
|
|
104
|
+
for (const srcPath of allFiles) {
|
|
105
|
+
const relativePath = path.relative(projectRoot, srcPath);
|
|
106
|
+
const destPath = path.join(distDir, relativePath);
|
|
107
|
+
const destDir = path.dirname(destPath);
|
|
108
|
+
|
|
109
|
+
// Create destination directory if needed
|
|
110
|
+
if (!fs.existsSync(destDir)) {
|
|
111
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Copy file
|
|
115
|
+
fs.copyFileSync(srcPath, destPath);
|
|
116
|
+
console.log(` ā ${relativePath}`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.log('ā
Project assets copied\n');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Transpile TypeScript files from jux/ to jux-dist/, preserving folder structure
|
|
124
|
+
*
|
|
125
|
+
* @param {string} srcDir - Source directory (jux/)
|
|
126
|
+
* @param {string} destDir - Destination directory (jux-dist/)
|
|
127
|
+
* @example
|
|
128
|
+
* // jux/samples/mypage.ts -> jux-dist/samples/mypage.js
|
|
129
|
+
* await transpileProjectTypeScript('jux/', 'jux-dist/');
|
|
130
|
+
*/
|
|
131
|
+
export async function transpileProjectTypeScript(srcDir, destDir) {
|
|
132
|
+
console.log('š· Transpiling TypeScript files...');
|
|
133
|
+
|
|
134
|
+
// Find all TypeScript files in the project
|
|
135
|
+
const tsFiles = findFiles(srcDir, '.ts');
|
|
136
|
+
|
|
137
|
+
if (tsFiles.length === 0) {
|
|
138
|
+
console.log(' No TypeScript files found in project');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(` Found ${tsFiles.length} TypeScript file(s)`);
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
// Build all TypeScript files with esbuild
|
|
146
|
+
await esbuild.build({
|
|
147
|
+
entryPoints: tsFiles,
|
|
148
|
+
bundle: false,
|
|
149
|
+
format: 'esm',
|
|
150
|
+
outdir: destDir,
|
|
151
|
+
outbase: srcDir,
|
|
152
|
+
platform: 'browser',
|
|
153
|
+
target: 'es2020',
|
|
154
|
+
loader: {
|
|
155
|
+
'.ts': 'ts'
|
|
156
|
+
},
|
|
157
|
+
logLevel: 'warning'
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Log each transpiled file
|
|
161
|
+
tsFiles.forEach(tsFile => {
|
|
162
|
+
const relativePath = path.relative(srcDir, tsFile);
|
|
163
|
+
const jsPath = relativePath.replace(/\.ts$/, '.js');
|
|
164
|
+
console.log(` ā ${relativePath} ā ${jsPath}`);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
console.log('ā
TypeScript transpiled\n');
|
|
168
|
+
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error('ā Failed to transpile TypeScript:', err.message);
|
|
171
|
+
throw err;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Copy presets folder from lib to dist (maintaining directory structure)
|
|
177
|
+
*
|
|
178
|
+
* @param {string} packageRoot - Source package root directory
|
|
179
|
+
* @param {string} distDir - Destination directory
|
|
180
|
+
*/
|
|
181
|
+
export async function copyPresetsToOutput(packageRoot, distDir) {
|
|
182
|
+
console.log('š¦ Copying presets...');
|
|
183
|
+
|
|
184
|
+
const presetsSrc = path.join(packageRoot, 'presets');
|
|
185
|
+
const presetsDest = path.join(distDir, 'presets');
|
|
186
|
+
|
|
187
|
+
if (!fs.existsSync(presetsSrc)) {
|
|
188
|
+
console.log(' No presets directory found');
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (fs.existsSync(presetsDest)) {
|
|
193
|
+
fs.rmSync(presetsDest, { recursive: true });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fs.mkdirSync(presetsDest, { recursive: true });
|
|
197
|
+
|
|
198
|
+
// Recursively copy entire presets directory structure
|
|
199
|
+
let copiedCount = 0;
|
|
200
|
+
|
|
201
|
+
function copyRecursive(src, dest) {
|
|
202
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
203
|
+
|
|
204
|
+
for (const entry of entries) {
|
|
205
|
+
const srcPath = path.join(src, entry.name);
|
|
206
|
+
const destPath = path.join(dest, entry.name);
|
|
207
|
+
|
|
208
|
+
if (entry.isDirectory()) {
|
|
209
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
210
|
+
copyRecursive(srcPath, destPath);
|
|
211
|
+
} else if (entry.isFile()) {
|
|
212
|
+
fs.copyFileSync(srcPath, destPath);
|
|
213
|
+
const relativePath = path.relative(presetsSrc, srcPath);
|
|
214
|
+
console.log(` ā ${relativePath}`);
|
|
215
|
+
copiedCount++;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
copyRecursive(presetsSrc, presetsDest);
|
|
221
|
+
|
|
222
|
+
console.log(`ā
Copied ${copiedCount} preset file(s)\n`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Recursively find files with a specific extension
|
|
227
|
+
*
|
|
228
|
+
* @param {string} dir - Directory to search
|
|
229
|
+
* @param {string} extension - File extension (e.g., '.ts')
|
|
230
|
+
* @param {string[]} fileList - Accumulator for found files
|
|
231
|
+
* @returns {string[]} Array of file paths
|
|
232
|
+
*/
|
|
233
|
+
function findFiles(dir, extension, fileList = []) {
|
|
234
|
+
const files = fs.readdirSync(dir);
|
|
235
|
+
|
|
236
|
+
files.forEach(file => {
|
|
237
|
+
const filePath = path.join(dir, file);
|
|
238
|
+
const stat = fs.statSync(filePath);
|
|
239
|
+
|
|
240
|
+
if (stat.isDirectory()) {
|
|
241
|
+
findFiles(filePath, extension, fileList);
|
|
242
|
+
} else if (file.endsWith(extension)) {
|
|
243
|
+
fileList.push(filePath);
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return fileList;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Copy non-TypeScript files (CSS, JSON, JS, SVG, etc.)
|
|
252
|
+
*
|
|
253
|
+
* @param {string} src - Source directory
|
|
254
|
+
* @param {string} dest - Destination directory
|
|
255
|
+
*/
|
|
256
|
+
function copyNonTsFiles(src, dest) {
|
|
257
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
258
|
+
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
const srcPath = path.join(src, entry.name);
|
|
261
|
+
const destPath = path.join(dest, entry.name);
|
|
262
|
+
|
|
263
|
+
if (entry.isDirectory()) {
|
|
264
|
+
if (!fs.existsSync(destPath)) {
|
|
265
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
copyNonTsFiles(srcPath, destPath);
|
|
268
|
+
} else if (entry.isFile()) {
|
|
269
|
+
const ext = path.extname(entry.name);
|
|
270
|
+
// Copy CSS, JSON, SVG, and JS files (but not .ts files)
|
|
271
|
+
if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
|
|
272
|
+
fs.copyFileSync(srcPath, destPath);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Find project files with specific extensions, excluding certain directories
|
|
280
|
+
*
|
|
281
|
+
* @param {string} dir - Directory to search
|
|
282
|
+
* @param {string[]} extensions - File extensions to find
|
|
283
|
+
* @param {string[]} fileList - Accumulator for found files
|
|
284
|
+
* @param {string} rootDir - Root directory for relative paths
|
|
285
|
+
* @param {string[]} excludeDirs - Directories to exclude
|
|
286
|
+
* @returns {string[]} Array of file paths
|
|
287
|
+
*/
|
|
288
|
+
function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
|
|
289
|
+
if (!fs.existsSync(dir)) return fileList;
|
|
290
|
+
|
|
291
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
292
|
+
|
|
293
|
+
for (const entry of entries) {
|
|
294
|
+
const fullPath = path.join(dir, entry.name);
|
|
295
|
+
|
|
296
|
+
if (entry.isDirectory()) {
|
|
297
|
+
// Skip excluded directories
|
|
298
|
+
if (excludeDirs.includes(entry.name)) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
|
|
302
|
+
} else {
|
|
303
|
+
// Check if file has one of the desired extensions
|
|
304
|
+
const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
|
|
305
|
+
if (hasExtension) {
|
|
306
|
+
fileList.push(fullPath);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return fileList;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Bundle all .jux files into a single router-based main.js
|
|
316
|
+
*
|
|
317
|
+
* @param {string} projectRoot - Source directory (jux/)
|
|
318
|
+
* @param {string} distDir - Destination directory (jux-dist/)
|
|
319
|
+
* @param {Object} options - Bundle options
|
|
320
|
+
* @param {string} options.routePrefix - Route prefix (e.g., '/experiments')
|
|
321
|
+
* @returns {Promise<string>} - Returns the generated filename (e.g., 'main.1234567890.js')
|
|
322
|
+
*/
|
|
323
|
+
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
324
|
+
const startTime = performance.now();
|
|
325
|
+
const { routePrefix = '' } = options;
|
|
326
|
+
|
|
327
|
+
console.log('š Bundling .jux files into router...');
|
|
328
|
+
|
|
329
|
+
const juxFiles = findFiles(projectRoot, '.jux');
|
|
330
|
+
|
|
331
|
+
if (juxFiles.length === 0) {
|
|
332
|
+
console.log(' No .jux files found');
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
console.log(` Found ${juxFiles.length} .jux file(s)`);
|
|
337
|
+
|
|
338
|
+
const fileTimings = [];
|
|
339
|
+
const views = [];
|
|
340
|
+
const routes = [];
|
|
341
|
+
const sharedModules = new Map(); // Track shared .jux modules
|
|
342
|
+
|
|
343
|
+
for (const juxFile of juxFiles) {
|
|
344
|
+
const fileStartTime = performance.now();
|
|
345
|
+
|
|
346
|
+
const relativePath = path.relative(projectRoot, juxFile);
|
|
347
|
+
const parsedPath = path.parse(relativePath);
|
|
348
|
+
|
|
349
|
+
const rawFunctionName = parsedPath.dir
|
|
350
|
+
? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}`
|
|
351
|
+
: parsedPath.name;
|
|
352
|
+
|
|
353
|
+
const cleanFunctionName = rawFunctionName
|
|
354
|
+
.replace(/[-_]/g, ' ')
|
|
355
|
+
.split(' ')
|
|
356
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
357
|
+
.join('');
|
|
358
|
+
|
|
359
|
+
const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
|
|
360
|
+
const cleanRoutePath = routePath.replace(/\/+/g, '/');
|
|
361
|
+
|
|
362
|
+
const juxContent = fs.readFileSync(juxFile, 'utf-8');
|
|
363
|
+
|
|
364
|
+
// Check if this file exports components (shared module)
|
|
365
|
+
const hasExports = /export\s+(const|let|function|class|{)/.test(juxContent);
|
|
366
|
+
|
|
367
|
+
if (hasExports) {
|
|
368
|
+
// Extract exports to shared modules section
|
|
369
|
+
sharedModules.set(relativePath, extractSharedModule(juxContent, rawFunctionName));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
|
|
373
|
+
|
|
374
|
+
views.push(viewFunction);
|
|
375
|
+
routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
|
|
376
|
+
|
|
377
|
+
const fileTime = performance.now() - fileStartTime;
|
|
378
|
+
fileTimings.push({ file: relativePath, time: fileTime });
|
|
379
|
+
|
|
380
|
+
const exportNote = hasExports ? ' [+exports]' : '';
|
|
381
|
+
console.log(` ā ${relativePath} ā ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ā
Show slowest files if any took >50ms
|
|
385
|
+
const slowFiles = fileTimings.filter(f => f.time > 50).sort((a, b) => b.time - a.time);
|
|
386
|
+
if (slowFiles.length > 0) {
|
|
387
|
+
console.log(`\n ā ļø Slowest files:`);
|
|
388
|
+
slowFiles.slice(0, 3).forEach(f => {
|
|
389
|
+
console.log(` ${f.file}: ${f.time.toFixed(0)}ms`);
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const bundleStartTime = performance.now();
|
|
394
|
+
const routerCode = generateRouterBundle(views, routes, sharedModules);
|
|
395
|
+
const bundleGenTime = performance.now() - bundleStartTime;
|
|
396
|
+
|
|
397
|
+
// ā
Use fixed filename (no timestamp)
|
|
398
|
+
const mainJsFilename = 'main.js';
|
|
399
|
+
const mainJsPath = path.join(distDir, mainJsFilename);
|
|
400
|
+
|
|
401
|
+
// Write to dist/main.js
|
|
402
|
+
const writeStartTime = performance.now();
|
|
403
|
+
fs.writeFileSync(mainJsPath, routerCode);
|
|
404
|
+
const writeTime = performance.now() - writeStartTime;
|
|
405
|
+
|
|
406
|
+
const totalTime = performance.now() - startTime;
|
|
407
|
+
|
|
408
|
+
console.log(` ā Generated: ${path.relative(projectRoot, mainJsPath)}`);
|
|
409
|
+
console.log(`\n š Bundle Statistics:`);
|
|
410
|
+
console.log(` Files processed: ${juxFiles.length}`);
|
|
411
|
+
console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
|
|
412
|
+
console.log(` Code generation: ${bundleGenTime.toFixed(0)}ms`);
|
|
413
|
+
console.log(` File write: ${writeTime.toFixed(0)}ms`);
|
|
414
|
+
console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
|
|
415
|
+
console.log('ā
Router bundle complete\n');
|
|
416
|
+
|
|
417
|
+
// ā
Return the filename so index.html can reference it
|
|
418
|
+
return mainJsFilename;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Extract and replace all string literals with placeholders
|
|
423
|
+
* Handles: template literals WITHOUT interpolations, single quotes, double quotes
|
|
424
|
+
* SKIPS: Template literals WITH ${} interpolations (those are dynamic code)
|
|
425
|
+
*/
|
|
426
|
+
function extractStrings(code) {
|
|
427
|
+
const strings = [];
|
|
428
|
+
let result = code;
|
|
429
|
+
|
|
430
|
+
// 1. Template literals WITHOUT interpolations (static strings only)
|
|
431
|
+
// Match backticks that DON'T contain ${
|
|
432
|
+
result = result.replace(/`([^`$\\]|\\[^$])*`/g, (match) => {
|
|
433
|
+
// Double-check it doesn't contain ${
|
|
434
|
+
if (!match.includes('${')) {
|
|
435
|
+
strings.push(match);
|
|
436
|
+
return `__STRING_${strings.length - 1}__`;
|
|
437
|
+
}
|
|
438
|
+
return match; // Leave dynamic templates alone
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// 2. Double-quoted strings (with escaped quotes)
|
|
442
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
|
|
443
|
+
strings.push(match);
|
|
444
|
+
return `__STRING_${strings.length - 1}__`;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
// 3. Single-quoted strings (with escaped quotes)
|
|
448
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, (match) => {
|
|
449
|
+
strings.push(match);
|
|
450
|
+
return `__STRING_${strings.length - 1}__`;
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
return { code: result, strings };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Restore string placeholders back to original strings
|
|
458
|
+
*/
|
|
459
|
+
function restoreStrings(code, strings) {
|
|
460
|
+
return code.replace(/__STRING_(\d+)__/g, (match, index) => {
|
|
461
|
+
const idx = parseInt(index, 10);
|
|
462
|
+
if (idx >= 0 && idx < strings.length) {
|
|
463
|
+
return strings[idx];
|
|
464
|
+
}
|
|
465
|
+
console.warn(`[Compiler] String placeholder ${match} not found (idx: ${idx}, available: ${strings.length})`);
|
|
466
|
+
return match; // Leave as-is for debugging
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Extract shared module exports from a .jux file
|
|
472
|
+
*
|
|
473
|
+
* @param {string} juxContent - Original .jux file content
|
|
474
|
+
* @param {string} moduleName - Module identifier
|
|
475
|
+
* @returns {string} Shared module code
|
|
476
|
+
*/
|
|
477
|
+
function extractSharedModule(juxContent, moduleName) {
|
|
478
|
+
const { code, strings } = extractStrings(juxContent);
|
|
479
|
+
|
|
480
|
+
// Remove ALL imports
|
|
481
|
+
let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
482
|
+
result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
483
|
+
result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
|
|
484
|
+
|
|
485
|
+
// Convert exports to declarations
|
|
486
|
+
result = result.replace(/export\s+const\s+/g, 'const ');
|
|
487
|
+
result = result.replace(/export\s+let\s+/g, 'let ');
|
|
488
|
+
result = result.replace(/export\s+function\s+/g, 'function ');
|
|
489
|
+
result = result.replace(/export\s+class\s+/g, 'class ');
|
|
490
|
+
result = result.replace(/export\s+default\s+/g, '');
|
|
491
|
+
result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
|
|
492
|
+
|
|
493
|
+
// Restore strings
|
|
494
|
+
return restoreStrings(result, strings);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Transform .jux file content into a view function
|
|
499
|
+
*
|
|
500
|
+
* @param {string} juxContent - Original .jux file content
|
|
501
|
+
* @param {string} functionName - Function name for the view
|
|
502
|
+
* @param {string} pageName - Page name for data attribute
|
|
503
|
+
* @param {string} relativePath - Relative path of the .jux file
|
|
504
|
+
* @param {Map} sharedModules - Map of shared module paths to their code
|
|
505
|
+
* @returns {string} View function code
|
|
506
|
+
*/
|
|
507
|
+
function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
|
|
508
|
+
const { code, strings } = extractStrings(juxContent);
|
|
509
|
+
|
|
510
|
+
// Remove ALL imports
|
|
511
|
+
let result = code.replace(/import\s+\{[^}]+\}\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
512
|
+
result = result.replace(/import\s+\*\s+as\s+\w+\s+from\s+__STRING_\d+__;?\s*/g, '');
|
|
513
|
+
result = result.replace(/import\s+__STRING_\d+__;?\s*/g, '');
|
|
514
|
+
|
|
515
|
+
// Handle exports
|
|
516
|
+
result = result.replace(/export\s+const\s+(\w+)\s*=/g, 'const $1 =');
|
|
517
|
+
result = result.replace(/export\s+let\s+(\w+)\s*=/g, 'let $1 =');
|
|
518
|
+
result = result.replace(/export\s+function\s+(\w+)/g, 'const $1 = function');
|
|
519
|
+
result = result.replace(/export\s+class\s+/g, 'class ');
|
|
520
|
+
result = result.replace(/export\s+default\s+/g, '');
|
|
521
|
+
result = result.replace(/export\s+\{([^}]+)\}\s*;?\s*/g, '');
|
|
522
|
+
|
|
523
|
+
// Replace render patterns
|
|
524
|
+
result = result.replace(/\.renderTo\(container\)/g, '.render("#app")');
|
|
525
|
+
result = result.replace(/\.render\(\s*\)/g, '.render("#app")');
|
|
526
|
+
|
|
527
|
+
// Restore strings
|
|
528
|
+
result = restoreStrings(result, strings);
|
|
529
|
+
|
|
530
|
+
const cleanName = functionName
|
|
531
|
+
.replace(/[-_]/g, ' ')
|
|
532
|
+
.split(' ')
|
|
533
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
534
|
+
.join('');
|
|
535
|
+
|
|
536
|
+
return `
|
|
537
|
+
// View: ${cleanName}
|
|
538
|
+
function ${cleanName}() {
|
|
539
|
+
${result}
|
|
540
|
+
|
|
541
|
+
return document.getElementById('app');
|
|
542
|
+
}`;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Generate complete router bundle with all views and routing logic
|
|
547
|
+
*
|
|
548
|
+
* @param {string[]} views - Array of view function code
|
|
549
|
+
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
550
|
+
* @param {Map} sharedModules - Map of shared module code
|
|
551
|
+
* @returns {string} Complete bundle code
|
|
552
|
+
*/
|
|
553
|
+
function generateRouterBundle(views, routes, sharedModules = new Map()) {
|
|
554
|
+
const libImport = './lib/jux.js';
|
|
555
|
+
|
|
556
|
+
// Generate shared modules section
|
|
557
|
+
const sharedModulesCode = Array.from(sharedModules.values())
|
|
558
|
+
.filter(code => code.trim())
|
|
559
|
+
.join('\n\n');
|
|
560
|
+
|
|
561
|
+
// Filter out empty views (shared modules)
|
|
562
|
+
const viewsCode = views.filter(v => v.trim()).join('\n\n');
|
|
563
|
+
|
|
564
|
+
const routeTable = routes
|
|
565
|
+
.map(r => ` '${r.path}': ${r.functionName}`)
|
|
566
|
+
.join(',\n');
|
|
567
|
+
|
|
568
|
+
return `// Generated Jux Router Bundle
|
|
569
|
+
import { jux, state } from '${libImport}';
|
|
570
|
+
|
|
571
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
572
|
+
// SHARED MODULES
|
|
573
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
574
|
+
|
|
575
|
+
${sharedModulesCode}
|
|
576
|
+
|
|
577
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
578
|
+
// VIEW FUNCTIONS
|
|
579
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
580
|
+
|
|
581
|
+
${viewsCode}
|
|
582
|
+
|
|
583
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
584
|
+
// 404 VIEW
|
|
585
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
586
|
+
|
|
587
|
+
function JuxNotFound() {
|
|
588
|
+
jux.heading(1, { text: '404 - Page Not Found' }).render('#app');
|
|
589
|
+
jux.paragraph('404-msg')
|
|
590
|
+
.text('The page you are looking for does not exist.')
|
|
591
|
+
.render('#app');
|
|
592
|
+
|
|
593
|
+
return document.getElementById('app');
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
597
|
+
// 403 VIEW
|
|
598
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
599
|
+
|
|
600
|
+
function JuxForbidden() {
|
|
601
|
+
jux.heading(1, { text: '403 - Forbidden' }).render('#app');
|
|
602
|
+
jux.paragraph('403-msg')
|
|
603
|
+
.text('You are not authorized to view this page.')
|
|
604
|
+
.render('#app');
|
|
605
|
+
|
|
606
|
+
return document.getElementById('app');
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
610
|
+
// ROUTE TABLE
|
|
611
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
612
|
+
|
|
613
|
+
const routes = {
|
|
614
|
+
${routeTable}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
618
|
+
// ROUTER CORE
|
|
619
|
+
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
620
|
+
|
|
621
|
+
const app = document.getElementById('app');
|
|
622
|
+
|
|
623
|
+
function render() {
|
|
624
|
+
let path = location.pathname;
|
|
625
|
+
|
|
626
|
+
// Try exact match first
|
|
627
|
+
let view = routes[path];
|
|
628
|
+
|
|
629
|
+
// If no match and path ends with /, try appending 'index'
|
|
630
|
+
if (!view && path.endsWith('/')) {
|
|
631
|
+
view = routes[path + 'index'] || routes[path.slice(0, -1) + '/index'];
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// If still no match and path doesn't end with /, try appending '/index'
|
|
635
|
+
if (!view && !path.endsWith('/')) {
|
|
636
|
+
view = routes[path + '/index'];
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Fall back to 404
|
|
640
|
+
view = view || JuxNotFound;
|
|
641
|
+
|
|
642
|
+
app.innerHTML = '';
|
|
643
|
+
app.removeAttribute('data-jux-page');
|
|
644
|
+
|
|
645
|
+
view();
|
|
646
|
+
|
|
647
|
+
const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
|
|
648
|
+
app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
document.addEventListener('click', e => {
|
|
652
|
+
const a = e.target.closest('a');
|
|
653
|
+
if (!a) return;
|
|
654
|
+
|
|
655
|
+
if (a.dataset.router === 'false') return;
|
|
656
|
+
|
|
657
|
+
const url = new URL(a.href);
|
|
658
|
+
if (url.origin !== location.origin) return;
|
|
659
|
+
|
|
660
|
+
e.preventDefault();
|
|
661
|
+
history.pushState({}, '', url.pathname);
|
|
662
|
+
render();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
window.addEventListener('popstate', render);
|
|
666
|
+
|
|
667
|
+
render();
|
|
668
|
+
`;
|
|
669
|
+
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Generate a unified index.html for router bundle
|
|
674
|
+
*
|
|
675
|
+
* @param {string} distDir - Destination directory
|
|
676
|
+
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
677
|
+
* @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
|
|
678
|
+
*/
|
|
679
|
+
export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
680
|
+
console.log('š Generating index.html...');
|
|
681
|
+
|
|
682
|
+
// Generate navigation links
|
|
683
|
+
const navLinks = routes
|
|
684
|
+
.map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
|
|
685
|
+
.join(' |\n');
|
|
686
|
+
|
|
687
|
+
const importMapScript = generateImportMapScript();
|
|
688
|
+
|
|
689
|
+
const html = `<!DOCTYPE html>
|
|
690
|
+
<html lang="en">
|
|
691
|
+
<head>
|
|
692
|
+
<meta charset="UTF-8">
|
|
693
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
694
|
+
<title>Jux Application</title>
|
|
695
|
+
</head>
|
|
696
|
+
<body data-theme="">
|
|
697
|
+
<!-- App container - router renders here -->
|
|
698
|
+
<div id="app"></div>
|
|
699
|
+
${importMapScript}
|
|
700
|
+
<script type="module" src="/${mainJsFilename}"></script>
|
|
701
|
+
</body>
|
|
702
|
+
</html>`;
|
|
703
|
+
|
|
704
|
+
const indexPath = path.join(distDir, 'index.html');
|
|
705
|
+
fs.writeFileSync(indexPath, html);
|
|
706
|
+
|
|
707
|
+
console.log(` ā Generated: index.html (references ${mainJsFilename})`);
|
|
708
|
+
console.log('ā
Index HTML complete\n');
|
|
709
|
+
}
|