juxscript 1.0.51 → 1.0.52

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.
Files changed (2) hide show
  1. package/machinery/config.js +50 -720
  2. package/package.json +1 -1
@@ -1,719 +1,19 @@
1
- import fs from 'fs';
2
1
  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
- `;
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
669
4
 
670
- }
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
671
7
 
672
8
  /**
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')
9
+ * Default JUX configuration
678
10
  */
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
11
  export const defaultConfig = {
712
12
  // Source directory for .jux files
713
13
  sourceDir: 'jux',
714
14
 
715
15
  // Output directory for built files (hidden like .git)
716
- distDir: '.jux', // ✅ Matches your example
16
+ distDir: '.jux',
717
17
 
718
18
  // Dev server ports
719
19
  ports: {
@@ -732,25 +32,55 @@ export const defaultConfig = {
732
32
  };
733
33
 
734
34
  /**
735
- * Load configuration and run bootstrap functions
35
+ * Load configuration from juxconfig.js or use defaults
36
+ * @param {string} projectRoot - Project root directory
37
+ * @returns {Promise<object>} Configuration object
736
38
  */
737
- function loadConfig() {
738
- // Load config from file
739
- const configPath = path.join(__dirname, '../config.json');
39
+ export async function loadConfig(projectRoot) {
40
+ const configPath = path.join(projectRoot, 'juxconfig.js');
41
+
740
42
  if (fs.existsSync(configPath)) {
741
- const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
742
- Object.assign(defaultConfig, config);
43
+ try {
44
+ const configModule = await import(`file://${configPath}`);
45
+ const userConfig = configModule.default || configModule;
46
+
47
+ return {
48
+ ...defaultConfig,
49
+ ...userConfig,
50
+ ports: { ...defaultConfig.ports, ...(userConfig.ports || {}) },
51
+ build: { ...defaultConfig.build, ...(userConfig.build || {}) }
52
+ };
53
+ } catch (err) {
54
+ console.warn(`âš ī¸ Failed to load juxconfig.js: ${err.message}`);
55
+ console.warn(` Using default configuration\n`);
56
+ return defaultConfig;
57
+ }
743
58
  }
744
-
745
- // Run bootstrap functions
746
- runBootstrap();
59
+
60
+ console.log('â„šī¸ No juxconfig.js found, using defaults');
61
+ return defaultConfig;
747
62
  }
748
63
 
749
64
  /**
750
- * Run bootstrap functions
65
+ * Run bootstrap functions from config
66
+ * @param {Function[]} bootstrapFunctions - Array of async functions to run
751
67
  */
752
- function runBootstrap() {
753
- console.log('🚀 Running bootstrap functions...');
754
- defaultConfig.bootstrap.forEach(fn => fn());
755
- console.log('✅ Bootstrap complete');
68
+ export async function runBootstrap(bootstrapFunctions = []) {
69
+ if (!Array.isArray(bootstrapFunctions) || bootstrapFunctions.length === 0) {
70
+ return;
71
+ }
72
+
73
+ console.log('🚀 Running bootstrap functions...\n');
74
+
75
+ for (const fn of bootstrapFunctions) {
76
+ if (typeof fn === 'function') {
77
+ try {
78
+ await fn();
79
+ } catch (err) {
80
+ console.error(`❌ Bootstrap function failed:`, err.message);
81
+ }
82
+ }
83
+ }
84
+
85
+ console.log('✅ Bootstrap complete\n');
756
86
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.51",
3
+ "version": "1.0.52",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",