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 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(`
@@ -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 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');
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
- 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');
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
- // 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');
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
- if (fs.existsSync(presetsSrc)) {
386
- let copiedCount = 0;
391
+ jux.hero('welcome', {
392
+ title: 'Welcome to JUX',
393
+ subtitle: 'Start building your app'
394
+ }).render('#app');
387
395
 
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
- }
396
+ const count = state(0);
411
397
 
412
- fs.mkdirSync(presetsDest, { recursive: true });
413
- copyRecursive(presetsSrc, presetsDest);
398
+ jux.button('increment')
399
+ .label('Click me!')
400
+ .bind('click', () => count.value++)
401
+ .style('margin: 2rem 0;')
402
+ .render('#app');
414
403
 
415
- if (copiedCount > 0) {
416
- console.log(`+ Copied ${copiedCount} preset file(s) to jux/presets/`);
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": "my-jux-project",
425
- "version": "1.0.0",
418
+ "name": projectName,
419
+ "version": "0.1.0",
426
420
  "type": "module",
427
421
  "scripts": {
428
- "build": "jux build",
429
- "serve": "jux serve"
422
+ "dev": "jux serve",
423
+ "build": "jux build"
430
424
  },
431
425
  "dependencies": {
432
- "juxscript": "^1.0.8"
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 = `jux-dist/
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 # 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');
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 init Initialize a new JUX project
478
- npx jux build Build router bundle to ./jux-dist/
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/ # 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
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 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
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
  })();
@@ -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,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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.49",
3
+ "version": "1.0.50",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",