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 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', `jux-dist/\nnode_modules/\n.DS_Store\n.env\n*.log\n`);
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.copyFileSync(configExampleSrc, configDest);
82
- console.log(` āœ“ juxconfig.js created`);
83
-
84
- // Also copy as .example (reference)
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.copyFileSync(configExampleSrc, configExampleDest);
87
- console.log(` āœ“ juxconfig.example.js created`);
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, 'jux');
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.join(this.packageRoot, 'lib');
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, 'jux-dist');
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 jux.jux as the starter index.jux (if it exists)
361
- const juxJuxSrc = path.join(PATHS.packageRoot, 'presets', 'jux.jux');
362
- const indexJuxDest = path.join(juxDir, 'index.jux');
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
- if (fs.existsSync(juxJuxSrc)) {
365
- fs.copyFileSync(juxJuxSrc, indexJuxDest);
366
- console.log('+ Created jux/index.jux from jux.jux template');
367
- } else {
368
- // Fallback to hey.jux if jux.jux doesn't exist
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
- // Copy entire presets folder to jux/presets/ (excluding jux.jux)
382
- const presetsSrc = path.join(PATHS.packageRoot, 'presets');
383
- const presetsDest = path.join(juxDir, 'presets');
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
- if (fs.existsSync(presetsSrc)) {
386
- let copiedCount = 0;
395
+ jux.hero('welcome', {
396
+ title: 'Welcome to JUX',
397
+ subtitle: 'Start building your app'
398
+ }).render('#app');
387
399
 
388
- function copyRecursive(src, dest) {
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
- fs.mkdirSync(presetsDest, { recursive: true });
413
- copyRecursive(presetsSrc, presetsDest);
402
+ jux.button('increment')
403
+ .label('Click me!')
404
+ .bind('click', () => count.value++)
405
+ .style('margin: 2rem 0;')
406
+ .render('#app');
414
407
 
415
- if (copiedCount > 0) {
416
- console.log(`+ Copied ${copiedCount} preset file(s) to jux/presets/`);
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": "my-jux-project",
425
- "version": "1.0.0",
422
+ "name": projectName,
423
+ "version": "0.1.0",
426
424
  "type": "module",
427
425
  "scripts": {
428
- "build": "jux build",
429
- "serve": "jux serve"
426
+ "dev": "jux serve",
427
+ "build": "jux build"
430
428
  },
431
429
  "dependencies": {
432
- "juxscript": "^1.0.8"
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 = `jux-dist/
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 # Install dependencies');
454
- console.log(' npx jux serve # Start dev server with hot reload\n');
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
- // āœ… Always builds router bundle
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
- // āœ… Always serves router bundle
464
- await buildProject(true);
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 start(httpPort, wsPort);
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 init Initialize a new JUX project
478
- npx jux build Build router bundle to ./jux-dist/
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
- ā”œā”€ā”€ jux/ # Your .jux source files
488
- │ ā”œā”€ā”€ index.jux # Entry point
489
- │ └── pages/ # Additional pages
490
- ā”œā”€ā”€ jux-dist/ # Build output (git-ignore this)
491
- ā”œā”€ā”€ server/ # Your backend
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 build # Build production bundle
507
- npx jux serve # Dev server (ports 3000/3001)
508
- npx jux serve 8080 # HTTP on 8080, WS on 3001
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
  })();
@@ -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-dist')
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
+ }
@@ -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 = './jux-dist') {
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, './jux-dist');
150
+ export async function start(httpPort = 3000, wsPort = 3001, distDir = './.jux') {
151
+ return serve(httpPort, wsPort, distDir);
152
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.49",
3
+ "version": "1.0.51",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",
@@ -1,5 +1,5 @@
1
1
  import { jux, state } from 'juxscript';
2
- import { initializeGrid } from '../presets/default/layout.jux';
2
+ import { initializeGrid } from 'layout.jux';
3
3
 
4
4
  // Initialize the grid layout - this executes the rendering
5
5
  const grid = initializeGrid();
@@ -1,7 +1,7 @@
1
1
  import { jux } from 'juxscript';
2
2
 
3
3
  // Import the layout styles - testing link.
4
- jux.include('../presets/default/layout.css');
4
+ jux.include('layout.css');
5
5
 
6
6
  // ═══════════════════════════════════════════════════════════════════
7
7
  // GRID LAYOUT - INITIALIZATION FUNCTION