juxscript 1.0.49 ā 1.0.51
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 +116 -111
- package/juxconfig.example.js +2 -2
- package/machinery/config.js +756 -0
- package/machinery/server.js +3 -3
- package/package.json +1 -1
- package/presets/{jux.jux ā default/index.jux} +1 -1
- package/presets/default/layout.jux +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(`
|
|
@@ -144,6 +147,10 @@ import {
|
|
|
144
147
|
} from '../machinery/compiler.js';
|
|
145
148
|
import { generateDocs } from '../machinery/doc-generator.js';
|
|
146
149
|
import { start } from '../machinery/server.js';
|
|
150
|
+
import { loadConfig, runBootstrap } from '../machinery/config.js';
|
|
151
|
+
|
|
152
|
+
// ā
Load config BEFORE setting up PATHS
|
|
153
|
+
const config = await loadConfig(process.cwd());
|
|
147
154
|
|
|
148
155
|
// CLEAR PATH CONTRACT - CONVENTIONS
|
|
149
156
|
const PATHS = {
|
|
@@ -155,25 +162,25 @@ const PATHS = {
|
|
|
155
162
|
|
|
156
163
|
// Where user's .jux source files live (CONVENTION: ./jux/)
|
|
157
164
|
get juxSource() {
|
|
158
|
-
return path.join(this.projectRoot,
|
|
165
|
+
return path.join(this.projectRoot, config.sourceDir);
|
|
159
166
|
},
|
|
160
167
|
|
|
161
168
|
// Where jux lib files are (components, layouts, etc.)
|
|
162
169
|
get juxLib() {
|
|
163
|
-
return path.
|
|
170
|
+
return path.resolve(__dirname, '..', 'lib');
|
|
164
171
|
},
|
|
165
172
|
|
|
166
173
|
// Where frontend build output goes (CONVENTION: ./jux-dist/)
|
|
167
174
|
get frontendDist() {
|
|
168
|
-
return path.join(this.projectRoot,
|
|
175
|
+
return path.join(this.projectRoot, config.distDir);
|
|
169
176
|
}
|
|
170
177
|
};
|
|
171
178
|
|
|
172
179
|
console.log('š JUX Paths:');
|
|
173
180
|
console.log(` Package: ${PATHS.packageRoot}`);
|
|
174
181
|
console.log(` Project: ${PATHS.projectRoot}`);
|
|
175
|
-
console.log(` Source: ${PATHS.juxSource}`);
|
|
176
|
-
console.log(` Output: ${PATHS.frontendDist}`);
|
|
182
|
+
console.log(` Source: ${PATHS.juxSource} (from config: ${config.sourceDir})`);
|
|
183
|
+
console.log(` Output: ${PATHS.frontendDist} (from config: ${config.distDir})`);
|
|
177
184
|
console.log(` Lib: ${PATHS.juxLib}\n`);
|
|
178
185
|
|
|
179
186
|
/**
|
|
@@ -205,7 +212,7 @@ function findJuxFiles(dir, fileList = []) {
|
|
|
205
212
|
*
|
|
206
213
|
* @param {boolean} isServe - Whether building for dev server
|
|
207
214
|
*/
|
|
208
|
-
async function buildProject(isServe = false) {
|
|
215
|
+
async function buildProject(isServe = false, wsPort = 3001) {
|
|
209
216
|
const buildStartTime = performance.now();
|
|
210
217
|
console.log('šØ Building JUX frontend...\n');
|
|
211
218
|
|
|
@@ -354,159 +361,157 @@ async function buildProject(isServe = false) {
|
|
|
354
361
|
process.exit(1);
|
|
355
362
|
}
|
|
356
363
|
|
|
357
|
-
// Create structure
|
|
364
|
+
// Create jux/ structure
|
|
358
365
|
fs.mkdirSync(juxDir, { recursive: true });
|
|
359
366
|
|
|
360
|
-
// Copy
|
|
361
|
-
const
|
|
362
|
-
|
|
367
|
+
// ā
Copy presets/default/ directly to jux/ (no presets subfolder!)
|
|
368
|
+
const defaultPresetSrc = path.join(PATHS.packageRoot, 'presets', 'default');
|
|
369
|
+
|
|
370
|
+
if (fs.existsSync(defaultPresetSrc)) {
|
|
371
|
+
console.log('š¦ Copying default preset boilerplate...');
|
|
372
|
+
|
|
373
|
+
const entries = fs.readdirSync(defaultPresetSrc, { withFileTypes: true });
|
|
374
|
+
let copiedCount = 0;
|
|
375
|
+
|
|
376
|
+
for (const entry of entries) {
|
|
377
|
+
const srcPath = path.join(defaultPresetSrc, entry.name);
|
|
378
|
+
const destPath = path.join(juxDir, entry.name);
|
|
363
379
|
|
|
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');
|
|
380
|
+
if (entry.isFile()) {
|
|
381
|
+
fs.copyFileSync(srcPath, destPath);
|
|
382
|
+
console.log(`+ Created jux/${entry.name}`);
|
|
383
|
+
copiedCount++;
|
|
384
|
+
}
|
|
378
385
|
}
|
|
386
|
+
|
|
387
|
+
console.log(`ā
Copied ${copiedCount} boilerplate file(s) to jux/\n`);
|
|
379
388
|
}
|
|
380
389
|
|
|
381
|
-
//
|
|
382
|
-
const
|
|
383
|
-
|
|
390
|
+
// ā
Create a simple index.jux if none exists
|
|
391
|
+
const indexJuxDest = path.join(juxDir, 'index.jux');
|
|
392
|
+
if (!fs.existsSync(indexJuxDest)) {
|
|
393
|
+
const basicContent = `import { jux, state } from 'juxscript';
|
|
384
394
|
|
|
385
|
-
|
|
386
|
-
|
|
395
|
+
jux.hero('welcome', {
|
|
396
|
+
title: 'Welcome to JUX',
|
|
397
|
+
subtitle: 'Start building your app'
|
|
398
|
+
}).render('#app');
|
|
387
399
|
|
|
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
|
-
}
|
|
400
|
+
const count = state(0);
|
|
411
401
|
|
|
412
|
-
|
|
413
|
-
|
|
402
|
+
jux.button('increment')
|
|
403
|
+
.label('Click me!')
|
|
404
|
+
.bind('click', () => count.value++)
|
|
405
|
+
.style('margin: 2rem 0;')
|
|
406
|
+
.render('#app');
|
|
414
407
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
408
|
+
jux.paragraph('counter')
|
|
409
|
+
.sync('text', count, val => \`Count: \${val}\`)
|
|
410
|
+
.render('#app');
|
|
411
|
+
`;
|
|
412
|
+
fs.writeFileSync(indexJuxDest, basicContent);
|
|
413
|
+
console.log('+ Created jux/index.jux');
|
|
418
414
|
}
|
|
419
415
|
|
|
420
416
|
// Create package.json if it doesn't exist
|
|
421
417
|
const pkgPath = path.join(PATHS.projectRoot, 'package.json');
|
|
422
418
|
if (!fs.existsSync(pkgPath)) {
|
|
419
|
+
const projectName = path.basename(PATHS.projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
|
|
420
|
+
|
|
423
421
|
const pkgContent = {
|
|
424
|
-
"name":
|
|
425
|
-
"version": "1.0
|
|
422
|
+
"name": projectName,
|
|
423
|
+
"version": "0.1.0",
|
|
426
424
|
"type": "module",
|
|
427
425
|
"scripts": {
|
|
428
|
-
"
|
|
429
|
-
"
|
|
426
|
+
"dev": "jux serve",
|
|
427
|
+
"build": "jux build"
|
|
430
428
|
},
|
|
431
429
|
"dependencies": {
|
|
432
|
-
"juxscript": "
|
|
430
|
+
"juxscript": "latest"
|
|
433
431
|
}
|
|
434
432
|
};
|
|
435
433
|
fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
|
|
436
434
|
console.log('+ Created package.json');
|
|
437
435
|
}
|
|
438
436
|
|
|
439
|
-
// Create .gitignore
|
|
437
|
+
// Create .gitignore with config.distDir
|
|
440
438
|
const gitignorePath = path.join(PATHS.projectRoot, '.gitignore');
|
|
441
|
-
const gitignoreContent =
|
|
442
|
-
node_modules/
|
|
443
|
-
.DS_Store
|
|
444
|
-
`;
|
|
439
|
+
const gitignoreContent = `${config.distDir}/\nnode_modules/\n.DS_Store\n`;
|
|
445
440
|
|
|
446
441
|
if (!fs.existsSync(gitignorePath)) {
|
|
447
442
|
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
448
443
|
console.log('+ Created .gitignore');
|
|
449
444
|
}
|
|
450
445
|
|
|
446
|
+
// ā
Copy juxconfig to root
|
|
447
|
+
const configExampleSrc = path.join(PATHS.packageRoot, 'juxconfig.example.js');
|
|
448
|
+
|
|
449
|
+
if (fs.existsSync(configExampleSrc)) {
|
|
450
|
+
const configDest = path.join(PATHS.projectRoot, 'juxconfig.js');
|
|
451
|
+
if (!fs.existsSync(configDest)) {
|
|
452
|
+
fs.copyFileSync(configExampleSrc, configDest);
|
|
453
|
+
console.log('+ Created juxconfig.js');
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const configExampleDest = path.join(PATHS.projectRoot, 'juxconfig.example.js');
|
|
457
|
+
if (!fs.existsSync(configExampleDest)) {
|
|
458
|
+
fs.copyFileSync(configExampleSrc, configExampleDest);
|
|
459
|
+
console.log('+ Created juxconfig.example.js (reference)');
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
451
463
|
console.log('\nā
JUX project initialized!\n');
|
|
464
|
+
console.log('Project structure:');
|
|
465
|
+
console.log(` ${config.sourceDir}/ # Your source files`);
|
|
466
|
+
console.log(` ${config.distDir}/ # Build output (git-ignored)`);
|
|
467
|
+
console.log(' juxconfig.js # Configuration');
|
|
468
|
+
console.log(' package.json # Dependencies\n');
|
|
452
469
|
console.log('Next steps:');
|
|
453
|
-
console.log(' npm install
|
|
454
|
-
console.log('
|
|
455
|
-
console.log('Check out the docs: https://juxscript.com/docs\n');
|
|
470
|
+
console.log(' npm install # Install dependencies');
|
|
471
|
+
console.log(' npm run dev # Start dev server\n');
|
|
456
472
|
|
|
457
473
|
} else if (command === 'build') {
|
|
458
|
-
// ā
|
|
474
|
+
// ā
Run bootstrap before build
|
|
475
|
+
await runBootstrap(config.bootstrap);
|
|
459
476
|
await buildProject(false);
|
|
460
477
|
console.log(`ā
Build complete: ${PATHS.frontendDist}`);
|
|
461
478
|
|
|
462
479
|
} else if (command === 'serve') {
|
|
463
|
-
// ā
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
// Parse port arguments: npx jux serve [httpPort] [wsPort]
|
|
467
|
-
const httpPort = parseInt(process.argv[3]) || 3000;
|
|
468
|
-
const wsPort = parseInt(process.argv[4]) || 3001;
|
|
480
|
+
// ā
Use ports from config
|
|
481
|
+
const httpPort = parseInt(process.argv[3]) || config.ports.http;
|
|
482
|
+
const wsPort = parseInt(process.argv[4]) || config.ports.ws;
|
|
469
483
|
|
|
470
|
-
await
|
|
484
|
+
await runBootstrap(config.bootstrap);
|
|
485
|
+
await buildProject(true, wsPort);
|
|
486
|
+
await start(httpPort, wsPort, PATHS.frontendDist);
|
|
471
487
|
|
|
472
488
|
} else {
|
|
473
489
|
console.log(`
|
|
474
490
|
JUX CLI - A JavaScript UX authorship platform
|
|
475
491
|
|
|
476
492
|
Usage:
|
|
477
|
-
npx jux
|
|
478
|
-
npx jux
|
|
493
|
+
npx jux create [name] Create a new JUX project
|
|
494
|
+
npx jux init Initialize JUX in current directory
|
|
495
|
+
npx jux build Build router bundle to ${config.distDir}/
|
|
479
496
|
npx jux serve [http] [ws] Start dev server with hot reload
|
|
480
497
|
|
|
481
|
-
Arguments:
|
|
482
|
-
[http] HTTP server port (default: 3000)
|
|
483
|
-
[ws] WebSocket port (default: 3001)
|
|
484
|
-
|
|
485
498
|
Project Structure:
|
|
486
499
|
my-project/
|
|
487
|
-
āāā
|
|
488
|
-
ā āāā
|
|
489
|
-
ā
|
|
490
|
-
|
|
491
|
-
āāā
|
|
500
|
+
āāā ${config.distDir}/ # Build output (git-ignored, like .git)
|
|
501
|
+
ā āāā lib/
|
|
502
|
+
ā āāā main.js
|
|
503
|
+
ā āāā index.html
|
|
504
|
+
āāā ${config.sourceDir}/ # Your .jux source files
|
|
505
|
+
ā āāā index.jux
|
|
506
|
+
ā āāā layout.css
|
|
507
|
+
ā āāā layout.jux
|
|
508
|
+
āāā juxconfig.js # Configuration
|
|
492
509
|
āāā package.json
|
|
493
510
|
|
|
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
511
|
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
|
|
512
|
+
npx jux create my-app Create new project
|
|
513
|
+
npx jux serve Dev server (ports 3000/3001)
|
|
514
|
+
npx jux serve 8080 8081 Custom ports
|
|
510
515
|
`);
|
|
511
516
|
}
|
|
512
517
|
})();
|
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,756 @@
|
|
|
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
|
+
}
|
|
710
|
+
|
|
711
|
+
export const defaultConfig = {
|
|
712
|
+
// Source directory for .jux files
|
|
713
|
+
sourceDir: 'jux',
|
|
714
|
+
|
|
715
|
+
// Output directory for built files (hidden like .git)
|
|
716
|
+
distDir: '.jux', // ā
Matches your example
|
|
717
|
+
|
|
718
|
+
// Dev server ports
|
|
719
|
+
ports: {
|
|
720
|
+
http: 3000,
|
|
721
|
+
ws: 3001
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
// Build options
|
|
725
|
+
build: {
|
|
726
|
+
minify: false,
|
|
727
|
+
sourcemap: true
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// Bootstrap functions (run before app starts)
|
|
731
|
+
bootstrap: []
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Load configuration and run bootstrap functions
|
|
736
|
+
*/
|
|
737
|
+
function loadConfig() {
|
|
738
|
+
// Load config from file
|
|
739
|
+
const configPath = path.join(__dirname, '../config.json');
|
|
740
|
+
if (fs.existsSync(configPath)) {
|
|
741
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
742
|
+
Object.assign(defaultConfig, config);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Run bootstrap functions
|
|
746
|
+
runBootstrap();
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Run bootstrap functions
|
|
751
|
+
*/
|
|
752
|
+
function runBootstrap() {
|
|
753
|
+
console.log('š Running bootstrap functions...');
|
|
754
|
+
defaultConfig.bootstrap.forEach(fn => fn());
|
|
755
|
+
console.log('ā
Bootstrap complete');
|
|
756
|
+
}
|
package/machinery/server.js
CHANGED
|
@@ -47,7 +47,7 @@ async function tryPort(startPort, maxAttempts = 5, reservedPorts = []) {
|
|
|
47
47
|
throw new Error(`Could not find available port after ${maxAttempts} attempts starting from ${startPort}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
async function serve(httpPort = 3000, wsPort = 3001, distDir = '
|
|
50
|
+
async function serve(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
|
|
51
51
|
const app = express();
|
|
52
52
|
const absoluteDistDir = path.resolve(distDir);
|
|
53
53
|
const projectRoot = path.resolve('.');
|
|
@@ -147,6 +147,6 @@ async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
|
|
|
147
147
|
return { server, httpPort: availableHttpPort, wsPort: availableWsPort };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
export async function start(httpPort = 3000, wsPort = 3001) {
|
|
151
|
-
return serve(httpPort, wsPort,
|
|
150
|
+
export async function start(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
|
|
151
|
+
return serve(httpPort, wsPort, distDir);
|
|
152
152
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jux } from 'juxscript';
|
|
2
2
|
|
|
3
3
|
// Import the layout styles - testing link.
|
|
4
|
-
jux.include('
|
|
4
|
+
jux.include('layout.css');
|
|
5
5
|
|
|
6
6
|
// āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
7
7
|
// GRID LAYOUT - INITIALIZATION FUNCTION
|