juxscript 1.0.39 → 1.0.41

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
@@ -13,10 +13,14 @@ import { start } from '../machinery/server.js';
13
13
  import path from 'path';
14
14
  import fs from 'fs';
15
15
  import { fileURLToPath } from 'url';
16
+ import { loadConfig, runBootstrap } from '../machinery/config.js';
16
17
 
17
18
  const __filename = fileURLToPath(import.meta.url);
18
19
  const __dirname = path.dirname(__filename);
19
20
 
21
+ // Load configuration first (before PATHS)
22
+ const config = await loadConfig(process.cwd());
23
+
20
24
  // CLEAR PATH CONTRACT - CONVENTIONS
21
25
  const PATHS = {
22
26
  // Where jux package is installed (in node_modules/juxscript or local dev)
@@ -25,20 +29,14 @@ const PATHS = {
25
29
  // Where the user's project root is (where they run `npx jux`)
26
30
  projectRoot: process.cwd(),
27
31
 
28
- // Where user's .jux source files live (CONVENTION: ./jux/)
29
- get juxSource() {
30
- return path.join(this.projectRoot, 'jux');
31
- },
32
+ // Where user's .jux source files live (from config or default)
33
+ juxSource: path.join(process.cwd(), config.sourceDir),
32
34
 
33
35
  // Where jux lib files are (components, layouts, etc.)
34
- get juxLib() {
35
- return path.join(this.packageRoot, 'lib');
36
- },
36
+ juxLib: path.resolve(__dirname, '..', 'lib'),
37
37
 
38
- // Where frontend build output goes (CONVENTION: ./jux-dist/)
39
- get frontendDist() {
40
- return path.join(this.projectRoot, 'jux-dist');
41
- }
38
+ // Where frontend build output goes (from config or default)
39
+ frontendDist: path.join(process.cwd(), config.distDir)
42
40
  };
43
41
 
44
42
  console.log('šŸ“ JUX Paths:');
@@ -80,8 +78,9 @@ function findJuxFiles(dir, fileList = []) {
80
78
  * Build the entire JUX project (ALWAYS uses router bundle)
81
79
  *
82
80
  * @param {boolean} isServe - Whether building for dev server
81
+ * @param {number} wsPort - WebSocket port for hot reload
83
82
  */
84
- async function buildProject(isServe = false) {
83
+ async function buildProject(isServe = false, wsPort = 3001) {
85
84
  const buildStartTime = performance.now();
86
85
  console.log('šŸ”Ø Building JUX frontend...\n');
87
86
 
@@ -101,14 +100,14 @@ async function buildProject(isServe = false) {
101
100
 
102
101
  // Step 1: Generate documentation
103
102
  const docsStartTime = performance.now();
104
- let docsTime = 0; // āœ… Declare with default value
103
+ let docsTime = 0;
105
104
  console.log('šŸ“š Generating documentation...');
106
105
  try {
107
106
  await generateDocs(PATHS.juxLib);
108
107
  docsTime = performance.now() - docsStartTime;
109
108
  console.log(`āœ… Documentation generated (${docsTime.toFixed(0)}ms)\n`);
110
109
  } catch (error) {
111
- docsTime = performance.now() - docsStartTime; // āœ… Still calculate time even on error
110
+ docsTime = performance.now() - docsStartTime;
112
111
  console.warn(`āš ļø Failed to generate docs (${docsTime.toFixed(0)}ms):`, error.message);
113
112
  }
114
113
 
@@ -124,7 +123,7 @@ async function buildProject(isServe = false) {
124
123
  const presetsTime = performance.now() - presetsStartTime;
125
124
  console.log(`ā±ļø Presets copy time: ${presetsTime.toFixed(0)}ms\n`);
126
125
 
127
- // Step 4: Copy project assets (CSS, JS, images)
126
+ // Step 4: Copy project assets
128
127
  const assetsStartTime = performance.now();
129
128
  await copyProjectAssets(PATHS.juxSource, PATHS.frontendDist);
130
129
  const assetsTime = performance.now() - assetsStartTime;
@@ -145,7 +144,6 @@ async function buildProject(isServe = false) {
145
144
  process.exit(1);
146
145
  }
147
146
 
148
- // āœ… Bundle and get the generated filename
149
147
  const bundleStartTime = performance.now();
150
148
  const mainJsFilename = await bundleJuxFilesToRouter(PATHS.juxSource, PATHS.frontendDist, {
151
149
  routePrefix: ''
@@ -175,16 +173,14 @@ async function buildProject(isServe = false) {
175
173
  };
176
174
  });
177
175
 
178
- // āœ… Generate unified index.html
179
176
  const indexStartTime = performance.now();
180
- generateIndexHtml(PATHS.frontendDist, routes, mainJsFilename);
177
+ generateIndexHtml(PATHS.frontendDist, routes, mainJsFilename, wsPort);
181
178
  const indexTime = performance.now() - indexStartTime;
182
179
 
183
180
  const totalBuildTime = performance.now() - buildStartTime;
184
181
 
185
182
  console.log(`\nāœ… Bundled ${projectJuxFiles.length} page(s) → ${PATHS.frontendDist}/${mainJsFilename}\n`);
186
183
 
187
- // āœ… Build summary with timing breakdown
188
184
  console.log(`šŸ“Š Build Summary:`);
189
185
  console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
190
186
  console.log(` Documentation: ${docsTime.toFixed(0)}ms`);
@@ -197,7 +193,6 @@ async function buildProject(isServe = false) {
197
193
  console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
198
194
  console.log(` Total build time: ${totalBuildTime.toFixed(0)}ms\n`);
199
195
 
200
- // Show usage
201
196
  if (!isServe) {
202
197
  console.log('šŸ“¦ Serve from your backend:');
203
198
  console.log(` Express: app.use(express.static('jux-dist'))`);
@@ -220,7 +215,106 @@ async function buildProject(isServe = false) {
220
215
  }
221
216
 
222
217
  (async () => {
223
- if (command === 'init') {
218
+ if (command === 'create') {
219
+ const projectName = process.argv[3] || 'my-jux-app';
220
+ const projectPath = path.join(PATHS.projectRoot, projectName);
221
+
222
+ console.log(`
223
+ ╔═══════════════════════════════════════════════════════╗
224
+ ā•‘ ā•‘
225
+ ā•‘ šŸŽØ Welcome to JUX ā•‘
226
+ ā•‘ ā•‘
227
+ ā•‘ Creating your new JUX project... ā•‘
228
+ ā•‘ ā•‘
229
+ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
230
+ `);
231
+
232
+ if (fs.existsSync(projectPath)) {
233
+ console.error(`āŒ Directory "${projectName}" already exists.`);
234
+ console.error(` Please choose a different name or remove the existing directory.\n`);
235
+ process.exit(1);
236
+ }
237
+
238
+ try {
239
+ const { execSync } = await import('child_process');
240
+
241
+ console.log(`šŸ“ Creating directory: ${projectName}`);
242
+ fs.mkdirSync(projectPath, { recursive: true });
243
+ process.chdir(projectPath);
244
+
245
+ console.log(`šŸ“¦ Initializing package.json...`);
246
+ const packageJson = {
247
+ name: projectName,
248
+ version: '0.1.0',
249
+ type: 'module',
250
+ scripts: {
251
+ dev: 'jux serve',
252
+ build: 'jux build'
253
+ },
254
+ dependencies: {
255
+ juxscript: '^1.0.8'
256
+ }
257
+ };
258
+ fs.writeFileSync('package.json', JSON.stringify(packageJson, null, 2));
259
+ console.log(` āœ“ package.json created`);
260
+
261
+ console.log(`\nšŸ“„ Installing juxscript...\n`);
262
+ try {
263
+ execSync('npm install', { stdio: 'inherit' });
264
+ console.log(`\n āœ“ Dependencies installed`);
265
+ } catch (err) {
266
+ console.error(`\n āš ļø npm install failed, but continuing...`);
267
+ }
268
+
269
+ console.log(`\nšŸŽØ Initializing JUX project structure...`);
270
+ execSync('npx jux init', { stdio: 'inherit' });
271
+
272
+ console.log(`\nšŸ“ Creating .gitignore...`);
273
+ fs.writeFileSync('.gitignore', `jux-dist/\nnode_modules/\n.DS_Store\n.env\n*.log\n`);
274
+ console.log(` āœ“ .gitignore created`);
275
+
276
+ console.log(`
277
+ ╔═══════════════════════════════════════════════════════╗
278
+ ā•‘ ā•‘
279
+ ā•‘ āœ… Project created successfully! ā•‘
280
+ ā•‘ ā•‘
281
+ ā•šā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•ā•
282
+
283
+ šŸ“‚ Your project is ready at: ./${projectName}
284
+
285
+ šŸš€ Next steps:
286
+ cd ${projectName}
287
+ npm run dev
288
+
289
+ šŸ“š Resources:
290
+ Documentation: [coming soon]
291
+ GitHub: https://github.com/juxscript/jux
292
+ Examples: https://github.com/juxscript/examples
293
+
294
+ ⭐ If you find JUX useful, please star us on GitHub!
295
+
296
+ šŸ”’ Security: Report issues to security@juxscript.com [placeholder]
297
+
298
+ Happy coding! šŸŽ‰
299
+ `);
300
+
301
+ } catch (err) {
302
+ console.error(`\nāŒ Project creation failed:`, err.message);
303
+
304
+ if (fs.existsSync(projectPath)) {
305
+ try {
306
+ process.chdir('..');
307
+ fs.rmSync(projectPath, { recursive: true, force: true });
308
+ console.log(` āœ“ Cleaned up failed project directory\n`);
309
+ } catch (cleanupErr) {
310
+ console.error(` āš ļø Could not clean up directory\n`);
311
+ }
312
+ }
313
+
314
+ process.exit(1);
315
+ }
316
+
317
+ } else if (command === 'init') {
224
318
  console.log('šŸŽØ Initializing JUX project...\n');
225
319
 
226
320
  const juxDir = PATHS.juxSource;
@@ -230,10 +324,8 @@ async function buildProject(isServe = false) {
230
324
  process.exit(1);
231
325
  }
232
326
 
233
- // Create structure
234
327
  fs.mkdirSync(juxDir, { recursive: true });
235
328
 
236
- // Copy jux.jux as the starter index.jux (if it exists)
237
329
  const juxJuxSrc = path.join(PATHS.packageRoot, 'presets', 'jux.jux');
238
330
  const indexJuxDest = path.join(juxDir, 'index.jux');
239
331
 
@@ -241,7 +333,6 @@ async function buildProject(isServe = false) {
241
333
  fs.copyFileSync(juxJuxSrc, indexJuxDest);
242
334
  console.log('+ Created jux/index.jux from jux.jux template');
243
335
  } else {
244
- // Fallback to hey.jux if jux.jux doesn't exist
245
336
  const heyJuxSrc = path.join(PATHS.packageRoot, 'presets', 'hey.jux');
246
337
  if (fs.existsSync(heyJuxSrc)) {
247
338
  fs.copyFileSync(heyJuxSrc, indexJuxDest);
@@ -254,7 +345,6 @@ async function buildProject(isServe = false) {
254
345
  }
255
346
  }
256
347
 
257
- // Copy entire presets folder to jux/presets/ (excluding jux.jux)
258
348
  const presetsSrc = path.join(PATHS.packageRoot, 'presets');
259
349
  const presetsDest = path.join(juxDir, 'presets');
260
350
 
@@ -268,7 +358,6 @@ async function buildProject(isServe = false) {
268
358
  const srcPath = path.join(src, entry.name);
269
359
  const destPath = path.join(dest, entry.name);
270
360
 
271
- // Skip jux.jux since we already copied it to index.jux
272
361
  if (entry.isFile() && entry.name === 'jux.jux') {
273
362
  continue;
274
363
  }
@@ -293,37 +382,51 @@ async function buildProject(isServe = false) {
293
382
  }
294
383
  }
295
384
 
296
- // Create package.json if it doesn't exist
297
385
  const pkgPath = path.join(PATHS.projectRoot, 'package.json');
298
386
  if (!fs.existsSync(pkgPath)) {
387
+ // āœ… Get project name from current directory or default
388
+ const projectName = path.basename(PATHS.projectRoot).toLowerCase().replace(/[^a-z0-9-]/g, '-');
389
+
299
390
  const pkgContent = {
300
- "name": "my-jux-project",
301
- "version": "1.0.0",
391
+ "name": projectName,
392
+ "version": "0.1.0",
302
393
  "type": "module",
303
394
  "scripts": {
304
- "build": "jux build",
305
- "serve": "jux serve"
395
+ "dev": "jux serve",
396
+ "build": "jux build"
306
397
  },
307
398
  "dependencies": {
308
- "juxscript": "^1.0.8"
399
+ "juxscript": "latest" // āœ… Always use latest stable
309
400
  }
310
401
  };
311
402
  fs.writeFileSync(pkgPath, JSON.stringify(pkgContent, null, 2));
312
403
  console.log('+ Created package.json');
313
404
  }
314
405
 
315
- // Create .gitignore
316
406
  const gitignorePath = path.join(PATHS.projectRoot, '.gitignore');
317
- const gitignoreContent = `jux-dist/
318
- node_modules/
319
- .DS_Store
320
- `;
407
+ const gitignoreContent = `jux-dist/\nnode_modules/\n.DS_Store\n`;
321
408
 
322
409
  if (!fs.existsSync(gitignorePath)) {
323
410
  fs.writeFileSync(gitignorePath, gitignoreContent);
324
411
  console.log('+ Created .gitignore');
325
412
  }
326
413
 
414
+ // āœ… Create actual juxconfig.js (not just example)
415
+ const configSrc = path.join(PATHS.packageRoot, 'juxconfig.example.js');
416
+ const configDest = path.join(PATHS.projectRoot, 'juxconfig.js');
417
+
418
+ if (fs.existsSync(configSrc) && !fs.existsSync(configDest)) {
419
+ fs.copyFileSync(configSrc, configDest);
420
+ console.log('+ Created juxconfig.js (customize as needed)');
421
+ }
422
+
423
+ // Also copy example as reference
424
+ const configExampleDest = path.join(PATHS.projectRoot, 'juxconfig.example.js');
425
+ if (fs.existsSync(configSrc) && !fs.existsSync(configExampleDest)) {
426
+ fs.copyFileSync(configSrc, configExampleDest);
427
+ console.log('+ Created juxconfig.example.js (reference)');
428
+ }
429
+
327
430
  console.log('\nāœ… JUX project initialized!\n');
328
431
  console.log('Next steps:');
329
432
  console.log(' npm install # Install dependencies');
@@ -331,18 +434,15 @@ node_modules/
331
434
  console.log('Check out the docs: https://juxscript.com/docs\n');
332
435
 
333
436
  } else if (command === 'build') {
334
- // āœ… Always builds router bundle
335
437
  await buildProject(false);
336
438
  console.log(`āœ… Build complete: ${PATHS.frontendDist}`);
337
439
 
338
440
  } else if (command === 'serve') {
339
- // āœ… Always serves router bundle
340
- await buildProject(true);
341
-
342
- // Parse port arguments: npx jux serve [httpPort] [wsPort]
343
- const httpPort = parseInt(process.argv[3]) || 3000;
344
- const wsPort = parseInt(process.argv[4]) || 3001;
441
+ const httpPort = parseInt(process.argv[3]) || config.ports.http;
442
+ const wsPort = parseInt(process.argv[4]) || config.ports.ws;
345
443
 
444
+ await runBootstrap(config.bootstrap);
445
+ await buildProject(true, wsPort);
346
446
  await start(httpPort, wsPort);
347
447
 
348
448
  } else {
@@ -350,39 +450,15 @@ node_modules/
350
450
  JUX CLI - A JavaScript UX authorship platform
351
451
 
352
452
  Usage:
353
- npx jux init Initialize a new JUX project
453
+ npx jux create [name] Create a new JUX project
454
+ npx jux init Initialize JUX in current directory
354
455
  npx jux build Build router bundle to ./jux-dist/
355
456
  npx jux serve [http] [ws] Start dev server with hot reload
356
457
 
357
- Arguments:
358
- [http] HTTP server port (default: 3000)
359
- [ws] WebSocket port (default: 3001)
360
-
361
- Project Structure:
362
- my-project/
363
- ā”œā”€ā”€ jux/ # Your .jux source files
364
- │ ā”œā”€ā”€ index.jux # Entry point
365
- │ └── pages/ # Additional pages
366
- ā”œā”€ā”€ jux-dist/ # Build output (git-ignore this)
367
- ā”œā”€ā”€ server/ # Your backend
368
- └── package.json
369
-
370
- Import Style:
371
- // In your project's .jux files
372
- import { jux, state } from 'juxscript';
373
- import 'juxscript/presets/notion.js';
374
-
375
- Getting Started:
376
- 1. npx jux init # Create project structure
377
- 2. npm install # Install dependencies
378
- 3. npx jux serve # Dev server with hot reload
379
- 4. Serve jux-dist/ from your backend
380
-
381
458
  Examples:
382
- npx jux build # Build production bundle
383
- npx jux serve # Dev server (ports 3000/3001)
384
- npx jux serve 8080 # HTTP on 8080, WS on 3001
385
- npx jux serve 8080 8081 # HTTP on 8080, WS on 8081
459
+ npx jux create my-app Create new project
460
+ npx jux serve Dev server (ports 3000/3001)
461
+ npx jux serve 8080 8081 Custom ports
386
462
  `);
387
463
  }
388
464
  })();
@@ -2053,5 +2053,5 @@
2053
2053
  }
2054
2054
  ],
2055
2055
  "version": "1.0.0",
2056
- "lastUpdated": "2026-01-28T17:24:41.869Z"
2056
+ "lastUpdated": "2026-01-28T20:10:47.892Z"
2057
2057
  }
@@ -673,16 +673,12 @@ render();
673
673
  *
674
674
  * @param {string} distDir - Destination directory
675
675
  * @param {Array<{path: string, functionName: string}>} routes - Route definitions
676
- * @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
676
+ * @param {string} mainJsFilename - The generated main.js filename
677
+ * @param {number} wsPort - WebSocket port for hot reload
677
678
  */
678
- export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
679
+ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js', wsPort = 3001) {
679
680
  console.log('šŸ“„ Generating index.html...');
680
681
 
681
- // Generate navigation links
682
- const navLinks = routes
683
- .map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
684
- .join(' |\n');
685
-
686
682
  const importMapScript = generateImportMapScript();
687
683
 
688
684
  const html = `<!DOCTYPE html>
@@ -697,6 +693,62 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
697
693
  <div id="app"></div>
698
694
  ${importMapScript}
699
695
  <script type="module" src="/${mainJsFilename}"></script>
696
+
697
+ <!-- Hot Reload Script -->
698
+ <script>
699
+ (function() {
700
+ const ws = new WebSocket('ws://' + location.hostname + ':${wsPort}');
701
+
702
+ ws.onopen = () => {
703
+ console.log('šŸ”Œ Hot reload connected');
704
+ };
705
+
706
+ ws.onmessage = (event) => {
707
+ const data = JSON.parse(event.data);
708
+
709
+ if (data.type === 'reload') {
710
+ console.log('šŸ”„ Hot reload triggered - reloading page...');
711
+ location.reload();
712
+ } else if (data.type === 'css-reload') {
713
+ console.log('šŸŽØ CSS hot reload:', data.path);
714
+
715
+ // Find all link tags pointing to this CSS file
716
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
717
+ links.forEach(link => {
718
+ if (link.href.includes(data.path)) {
719
+ const newLink = link.cloneNode();
720
+ newLink.href = data.path + '?t=' + Date.now();
721
+ link.parentNode.insertBefore(newLink, link.nextSibling);
722
+ setTimeout(() => link.remove(), 100);
723
+ }
724
+ });
725
+
726
+ // Also reload any @import in style tags
727
+ const styles = document.querySelectorAll('style');
728
+ styles.forEach(style => {
729
+ if (style.textContent.includes(data.path)) {
730
+ style.textContent = style.textContent.replace(
731
+ new RegExp(data.path + '(\\\\?t=\\\\d+)?', 'g'),
732
+ data.path + '?t=' + Date.now()
733
+ );
734
+ }
735
+ });
736
+ }
737
+ };
738
+
739
+ ws.onclose = () => {
740
+ console.log('šŸ”Œ Hot reload disconnected');
741
+ // Try to reconnect after 1 second
742
+ setTimeout(() => {
743
+ location.reload();
744
+ }, 1000);
745
+ };
746
+
747
+ ws.onerror = (error) => {
748
+ console.error('šŸ”Œ Hot reload error:', error);
749
+ };
750
+ })();
751
+ </script>
700
752
  </body>
701
753
  </html>`;
702
754
 
@@ -0,0 +1,94 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ /**
5
+ * Default JUX configuration
6
+ */
7
+ export const defaultConfig = {
8
+ // Source directory for .jux files
9
+ sourceDir: 'jux',
10
+
11
+ // Output directory for built files
12
+ distDir: 'jux-dist',
13
+
14
+ // Dev server ports
15
+ ports: {
16
+ http: 3000,
17
+ ws: 3001
18
+ },
19
+
20
+ // Build options
21
+ build: {
22
+ minify: false,
23
+ sourcemap: true
24
+ },
25
+
26
+ // Bootstrap functions (run before app starts)
27
+ bootstrap: []
28
+ };
29
+
30
+ /**
31
+ * Load juxconfig.js from project root
32
+ * @param {string} projectRoot - Project root directory
33
+ * @returns {object} Merged configuration
34
+ */
35
+ export async function loadConfig(projectRoot) {
36
+ const configPath = path.join(projectRoot, 'juxconfig.js');
37
+
38
+ if (!fs.existsSync(configPath)) {
39
+ console.log('ā„¹ļø No juxconfig.js found, using defaults');
40
+ return defaultConfig;
41
+ }
42
+
43
+ try {
44
+ console.log(`šŸ“‹ Loading config from: ${configPath}`);
45
+
46
+ // Dynamic import for ES modules
47
+ const configUrl = `file://${configPath}`;
48
+ const { default: userConfig } = await import(configUrl);
49
+
50
+ // Merge with defaults
51
+ const config = {
52
+ ...defaultConfig,
53
+ ...userConfig,
54
+ ports: {
55
+ ...defaultConfig.ports,
56
+ ...(userConfig.ports || {})
57
+ },
58
+ build: {
59
+ ...defaultConfig.build,
60
+ ...(userConfig.build || {})
61
+ }
62
+ };
63
+
64
+ console.log(` āœ“ Config loaded`);
65
+ console.log(` Source: ${config.sourceDir}`);
66
+ console.log(` Output: ${config.distDir}\n`);
67
+
68
+ return config;
69
+ } catch (err) {
70
+ console.warn(`āš ļø Failed to load juxconfig.js:`, err.message);
71
+ console.warn(` Using default configuration\n`);
72
+ return defaultConfig;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Run bootstrap functions from config
78
+ * @param {Array<Function>} bootstrapFns - Array of bootstrap functions
79
+ */
80
+ export async function runBootstrap(bootstrapFns) {
81
+ if (!bootstrapFns || bootstrapFns.length === 0) return;
82
+
83
+ console.log(`šŸš€ Running ${bootstrapFns.length} bootstrap function(s)...`);
84
+
85
+ for (const fn of bootstrapFns) {
86
+ try {
87
+ await fn();
88
+ } catch (err) {
89
+ console.error(` āŒ Bootstrap function failed:`, err.message);
90
+ }
91
+ }
92
+
93
+ console.log(` āœ“ Bootstrap complete\n`);
94
+ }
@@ -149,4 +149,66 @@ async function serve(httpPort = 3000, wsPort = 3001, distDir = './jux-dist') {
149
149
 
150
150
  export async function start(httpPort = 3000, wsPort = 3001) {
151
151
  return serve(httpPort, wsPort, './jux-dist');
152
+ }
153
+
154
+ function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
155
+ const html = `<!DOCTYPE html>
156
+ <html lang="en">
157
+ <head>
158
+ <meta charset="UTF-8">
159
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
160
+ <title>Jux Application</title>
161
+ </head>
162
+ <body data-theme="">
163
+ <div id="app"></div>
164
+ ${importMapScript}
165
+ <script type="module" src="/${mainJsFilename}"></script>
166
+
167
+ <!-- Hot Reload Script -->
168
+ <script>
169
+ (function() {
170
+ const ws = new WebSocket('ws://' + location.hostname + ':${wsPort}');
171
+
172
+ ws.onmessage = (event) => {
173
+ const data = JSON.parse(event.data);
174
+
175
+ if (data.type === 'reload') {
176
+ console.log('šŸ”„ Hot reload triggered');
177
+ location.reload();
178
+ } else if (data.type === 'css-reload') {
179
+ console.log('šŸŽØ CSS hot reload:', data.path);
180
+
181
+ // Find all link tags pointing to this CSS file
182
+ const links = document.querySelectorAll('link[rel="stylesheet"]');
183
+ links.forEach(link => {
184
+ if (link.href.includes(data.path)) {
185
+ const newLink = link.cloneNode();
186
+ newLink.href = data.path + '?t=' + Date.now();
187
+ link.parentNode.insertBefore(newLink, link.nextSibling);
188
+ setTimeout(() => link.remove(), 100);
189
+ }
190
+ });
191
+
192
+ // Also reload any @import in style tags
193
+ const styles = document.querySelectorAll('style');
194
+ styles.forEach(style => {
195
+ if (style.textContent.includes(data.path)) {
196
+ style.textContent = style.textContent.replace(
197
+ new RegExp(data.path + '(\\?t=\\d+)?', 'g'),
198
+ data.path + '?t=' + Date.now()
199
+ );
200
+ }
201
+ });
202
+ }
203
+ };
204
+
205
+ ws.onclose = () => {
206
+ console.log('šŸ”Œ Hot reload disconnected');
207
+ };
208
+ })();
209
+ </script>
210
+ </body>
211
+ </html>`;
212
+
213
+ return html;
152
214
  }
@@ -1,10 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import {
4
- copyLibToOutput,
5
- bundleJuxFilesToRouter,
6
- generateIndexHtml
7
- } from './compiler.js';
3
+ import { bundleJuxFilesToRouter } from './compiler.js';
8
4
 
9
5
  let isRebuilding = false;
10
6
  let rebuildQueued = false;
@@ -119,53 +115,130 @@ async function fullRebuild(juxSource, distDir) {
119
115
  * Start watching for file changes and rebuild on change
120
116
  */
121
117
  export function startWatcher(juxSource, distDir, wsClients) {
122
- console.log(`šŸ‘€ Watching: ${juxSource}`);
123
-
124
- const watcher = fs.watch(juxSource, { recursive: true }, async (eventType, filename) => {
125
- // Ignore non-.jux files and certain patterns
126
- if (!filename ||
127
- !filename.endsWith('.jux') ||
128
- filename.includes('node_modules') ||
129
- filename.includes('jux-dist') ||
130
- filename.startsWith('.')) {
131
- return;
132
- }
118
+ console.log(`šŸ‘€ Watching for changes in: ${juxSource}`);
119
+
120
+ // Debounce map to prevent multiple rapid triggers
121
+ const debounceTimers = new Map();
133
122
 
134
- // Debounce: If already rebuilding, queue another rebuild
135
- if (isRebuilding) {
136
- rebuildQueued = true;
137
- return;
123
+ function debounce(key, fn, delay = 100) {
124
+ if (debounceTimers.has(key)) {
125
+ clearTimeout(debounceTimers.get(key));
138
126
  }
127
+ debounceTimers.set(key, setTimeout(() => {
128
+ debounceTimers.delete(key);
129
+ fn();
130
+ }, delay));
131
+ }
132
+
133
+ // Recursively watch directories
134
+ function watchRecursive(dir) {
135
+ if (!fs.existsSync(dir)) return;
136
+
137
+ try {
138
+ fs.watch(dir, { recursive: true }, (eventType, filename) => {
139
+ if (!filename) return;
140
+
141
+ const filePath = path.join(dir, filename);
142
+ const ext = path.extname(filename);
143
+
144
+ // Skip certain patterns
145
+ if (filename.includes('node_modules') ||
146
+ filename.includes('jux-dist') ||
147
+ filename.includes('.git') ||
148
+ filename.startsWith('.')) {
149
+ return;
150
+ }
151
+
152
+ // Debounce to avoid multiple rapid events
153
+ debounce(filePath, async () => {
154
+ // āœ… Handle CSS files
155
+ if (ext === '.css') {
156
+ console.log(`\nšŸŽØ CSS changed: ${filename}`);
157
+
158
+ try {
159
+ const relativePath = path.relative(juxSource, filePath);
160
+ const destPath = path.join(distDir, relativePath);
161
+ const destDir = path.dirname(destPath);
162
+
163
+ if (!fs.existsSync(destDir)) {
164
+ fs.mkdirSync(destDir, { recursive: true });
165
+ }
166
+
167
+ fs.copyFileSync(filePath, destPath);
168
+ console.log(` āœ“ Copied to: ${path.relative(process.cwd(), destPath)}`);
169
+
170
+ // Notify browser to reload CSS
171
+ wsClients.forEach(client => {
172
+ if (client.readyState === 1) {
173
+ client.send(JSON.stringify({
174
+ type: 'css-reload',
175
+ path: `/${relativePath}`
176
+ }));
177
+ }
178
+ });
179
+
180
+ console.log(` šŸ”„ Browser CSS reloaded\n`);
181
+ } catch (err) {
182
+ console.error(` āŒ CSS copy failed:`, err.message);
183
+ }
184
+ return;
185
+ }
186
+
187
+ // āœ… Handle .jux files
188
+ if (ext === '.jux') {
189
+ console.log(`\nšŸ“ File changed: ${filename}`);
190
+ console.log(' šŸ”Ø Rebuilding...');
191
+
192
+ try {
193
+ await bundleJuxFilesToRouter(juxSource, distDir, { routePrefix: '' });
194
+
195
+ wsClients.forEach(client => {
196
+ if (client.readyState === 1) {
197
+ client.send(JSON.stringify({ type: 'reload' }));
198
+ }
199
+ });
200
+
201
+ console.log(' āœ… Rebuild complete');
202
+ console.log(' šŸ”„ Browser reloaded\n');
203
+ } catch (err) {
204
+ console.error(' āŒ Rebuild failed:', err.message);
205
+ }
206
+ }
207
+
208
+ // āœ… Handle .js files (non-_dev-imports.js)
209
+ if (ext === '.js' && !filename.includes('_dev-imports.js')) {
210
+ console.log(`\nšŸ“¦ JS asset changed: ${filename}`);
139
211
 
140
- isRebuilding = true;
141
- console.log(`\nšŸ“ File changed: ${filename}`);
212
+ try {
213
+ const relativePath = path.relative(juxSource, filePath);
214
+ const destPath = path.join(distDir, relativePath);
215
+ const destDir = path.dirname(destPath);
142
216
 
143
- // Rebuild the entire bundle
144
- const success = await fullRebuild(juxSource, distDir);
217
+ if (!fs.existsSync(destDir)) {
218
+ fs.mkdirSync(destDir, { recursive: true });
219
+ }
145
220
 
146
- isRebuilding = false;
221
+ fs.copyFileSync(filePath, destPath);
222
+ console.log(` āœ“ Copied to: ${path.relative(process.cwd(), destPath)}`);
147
223
 
148
- // Notify all WebSocket clients to reload
149
- if (success && wsClients && wsClients.length > 0) {
150
- console.log(`šŸ”Œ Notifying ${wsClients.length} client(s) to reload`);
224
+ wsClients.forEach(client => {
225
+ if (client.readyState === 1) {
226
+ client.send(JSON.stringify({ type: 'reload' }));
227
+ }
228
+ });
151
229
 
152
- // āœ… Add small delay to ensure file is fully written
153
- setTimeout(() => {
154
- wsClients.forEach(client => {
155
- if (client.readyState === 1) { // OPEN
156
- client.send(JSON.stringify({ type: 'reload' }));
230
+ console.log(` šŸ”„ Browser reloaded\n`);
231
+ } catch (err) {
232
+ console.error(` āŒ JS copy failed:`, err.message);
233
+ }
157
234
  }
158
235
  });
159
- }, 100);
160
- }
161
-
162
- // Process queued rebuild if needed
163
- if (rebuildQueued) {
164
- rebuildQueued = false;
165
- console.log('šŸ”„ Processing queued rebuild...');
166
- setTimeout(() => watcher.emit('change', 'change', filename), 500);
236
+ });
237
+ } catch (err) {
238
+ console.error(` āš ļø Failed to watch ${dir}:`, err.message);
167
239
  }
168
- });
240
+ }
169
241
 
170
- return watcher;
242
+ // Start watching
243
+ watchRecursive(juxSource);
171
244
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.39",
3
+ "version": "1.0.41",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",
@@ -105,6 +105,15 @@ body {
105
105
  cursor: pointer;
106
106
  }
107
107
 
108
+ /* āœ… Fix h1 inside logo - reset default heading styles */
109
+ #appheader-logo h1 {
110
+ font-size: 1.25rem;
111
+ font-weight: 700;
112
+ margin: 0;
113
+ line-height: 1;
114
+ letter-spacing: -0.02em;
115
+ }
116
+
108
117
  #appheader-nav {
109
118
  flex: 1;
110
119
  display: flex;
@@ -1,9 +1,8 @@
1
1
  import { jux } from 'juxscript';
2
2
 
3
- // Import the layout styles
3
+ // Import the layout styles - testing link.
4
4
  jux.include('../presets/default/layout.css');
5
5
 
6
-
7
6
  // ═══════════════════════════════════════════════════════════════════
8
7
  // GRID LAYOUT - INITIALIZATION FUNCTION
9
8
  // ═══════════════════════════════════════════════════════════════════