juxscript 1.0.61 → 1.0.63
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 +161 -293
- package/docs/v2comps/HEADLESS.md +83 -0
- package/docs/v2comps/ISOMORPHISM.md +10 -0
- package/juxconfig.example.js +63 -58
- package/lib/componentsv2/base/BaseEngine.js +258 -0
- package/lib/componentsv2/base/BaseEngine.js.map +1 -0
- package/lib/componentsv2/base/BaseEngine.ts +303 -0
- package/lib/componentsv2/base/BaseSkin.js +108 -0
- package/lib/componentsv2/base/BaseSkin.js.map +1 -0
- package/lib/componentsv2/base/BaseSkin.ts +137 -0
- package/lib/componentsv2/base/GlobalBus.js +56 -0
- package/lib/componentsv2/base/GlobalBus.js.map +1 -0
- package/lib/componentsv2/base/GlobalBus.ts +60 -0
- package/lib/componentsv2/base/State.js +68 -0
- package/lib/componentsv2/base/State.js.map +1 -0
- package/lib/componentsv2/base/State.ts +62 -0
- package/lib/componentsv2/grid/component.js +41 -0
- package/lib/componentsv2/grid/component.js.map +1 -0
- package/lib/componentsv2/grid/component.ts +67 -0
- package/lib/componentsv2/grid/engine.js +73 -0
- package/lib/componentsv2/grid/engine.js.map +1 -0
- package/lib/componentsv2/grid/engine.ts +110 -0
- package/lib/componentsv2/grid/skin.js +95 -0
- package/lib/componentsv2/grid/skin.js.map +1 -0
- package/lib/componentsv2/grid/skin.ts +105 -0
- package/lib/componentsv2/grid/structure.css +58 -0
- package/lib/componentsv2/index.js +218 -0
- package/lib/componentsv2/index.js.map +1 -0
- package/lib/componentsv2/index.ts +253 -0
- package/lib/componentsv2/input/component.js +21 -0
- package/lib/componentsv2/input/component.js.map +1 -0
- package/lib/componentsv2/input/component.ts +28 -0
- package/lib/componentsv2/input/engine.js +50 -0
- package/lib/componentsv2/input/engine.js.map +1 -0
- package/lib/componentsv2/input/engine.ts +76 -0
- package/lib/componentsv2/input/skin.js +91 -0
- package/lib/componentsv2/input/skin.js.map +1 -0
- package/lib/componentsv2/input/skin.ts +91 -0
- package/lib/componentsv2/input/structure.css +47 -0
- package/lib/componentsv2/list/component.js +83 -0
- package/lib/componentsv2/list/component.js.map +1 -0
- package/lib/componentsv2/list/component.ts +97 -0
- package/lib/componentsv2/list/engine.js +261 -0
- package/lib/componentsv2/list/engine.js.map +1 -0
- package/lib/componentsv2/list/engine.ts +345 -0
- package/lib/componentsv2/list/skin.js +343 -0
- package/lib/componentsv2/list/skin.js.map +1 -0
- package/lib/componentsv2/list/skin.ts +367 -0
- package/lib/componentsv2/list/structure.css +359 -0
- package/lib/componentsv2/plugins/ClientSQLitePlugin.js +130 -0
- package/lib/componentsv2/plugins/ClientSQLitePlugin.js.map +1 -0
- package/lib/componentsv2/plugins/ClientSQLitePlugin.ts +154 -0
- package/lib/componentsv2/plugins/IndexedDBPlugin.js +75 -0
- package/lib/componentsv2/plugins/IndexedDBPlugin.js.map +1 -0
- package/lib/componentsv2/plugins/IndexedDBPlugin.ts +96 -0
- package/lib/componentsv2/plugins/LocalStoragePlugin.js +65 -0
- package/lib/componentsv2/plugins/LocalStoragePlugin.js.map +1 -0
- package/lib/componentsv2/plugins/LocalStoragePlugin.ts +86 -0
- package/lib/componentsv2/plugins/ServerSQLitePlugin.js +70 -0
- package/lib/componentsv2/plugins/ServerSQLitePlugin.js.map +1 -0
- package/lib/componentsv2/plugins/ServerSQLitePlugin.ts +99 -0
- package/lib/componentsv2/stubs/ComponentComposition.ts.stub +32 -0
- package/lib/componentsv2/stubs/ComponentEngine.ts.stub +36 -0
- package/lib/componentsv2/stubs/ComponentSkin.ts.stub +34 -0
- package/lib/componentsv2/stubs/ComponentStructure.css.stub +13 -0
- package/lib/componentsv2/tools/CreateSkin.js +62 -0
- package/lib/componentsv2/tools/DocSpam.js +134 -0
- package/lib/componentsv2/tools/FluencyAudit.js +141 -0
- package/lib/componentsv2/tools/OptionsAudit.js +177 -0
- package/lib/componentsv2/tools/Scaffold.js +140 -0
- package/lib/utils/fetch.js +428 -0
- package/lib/utils/fetch.js.map +1 -0
- package/machinery/build.js +2 -1
- package/machinery/compiler.js +200 -37
- package/machinery/config.js +93 -6
- package/machinery/diagnose.js +72 -0
- package/machinery/jux-module-pattern.md +118 -0
- package/machinery/server.js +23 -7
- package/machinery/verifier.js +143 -0
- package/machinery/watcher.js +53 -64
- package/package.json +11 -2
- package/lib/components/alert.ts +0 -200
- package/lib/components/app.ts +0 -258
- package/lib/components/badge.ts +0 -101
- package/lib/components/base/BaseComponent.ts +0 -417
- package/lib/components/base/FormInput.ts +0 -227
- package/lib/components/button.ts +0 -178
- package/lib/components/card.ts +0 -173
- package/lib/components/chart.ts +0 -231
- package/lib/components/checkbox.ts +0 -242
- package/lib/components/code.ts +0 -123
- package/lib/components/container.ts +0 -140
- package/lib/components/data.ts +0 -135
- package/lib/components/datepicker.ts +0 -234
- package/lib/components/dialog.ts +0 -172
- package/lib/components/divider.ts +0 -100
- package/lib/components/dropdown.ts +0 -186
- package/lib/components/element.ts +0 -267
- package/lib/components/error-handler.ts +0 -285
- package/lib/components/fileupload.ts +0 -309
- package/lib/components/grid.ts +0 -291
- package/lib/components/guard.ts +0 -92
- package/lib/components/heading.ts +0 -96
- package/lib/components/helpers.ts +0 -41
- package/lib/components/hero.ts +0 -224
- package/lib/components/icon.ts +0 -160
- package/lib/components/icons.ts +0 -175
- package/lib/components/include.ts +0 -440
- package/lib/components/input.ts +0 -457
- package/lib/components/list.ts +0 -419
- package/lib/components/loading.ts +0 -100
- package/lib/components/menu.ts +0 -260
- package/lib/components/modal.ts +0 -239
- package/lib/components/nav.ts +0 -257
- package/lib/components/paragraph.ts +0 -97
- package/lib/components/progress.ts +0 -139
- package/lib/components/radio.ts +0 -278
- package/lib/components/req.ts +0 -302
- package/lib/components/script.ts +0 -43
- package/lib/components/select.ts +0 -252
- package/lib/components/sidebar.ts +0 -167
- package/lib/components/style.ts +0 -43
- package/lib/components/switch.ts +0 -246
- package/lib/components/table.ts +0 -1249
- package/lib/components/tabs.ts +0 -250
- package/lib/components/theme-toggle.ts +0 -300
- package/lib/components/token-calculator.ts +0 -313
- package/lib/components/tooltip.ts +0 -144
- package/lib/components/view.ts +0 -190
- package/lib/components/write.ts +0 -272
- package/lib/jux.ts +0 -365
- package/lib/layouts/default.css +0 -260
- package/lib/layouts/figma.css +0 -334
- package/lib/reactivity/state.ts +0 -78
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/presets/default/all.jux +0 -343
- package/presets/default/index.jux +0 -90
- package/presets/default/layout.css +0 -1612
- package/presets/default/layout.jux +0 -55
package/machinery/compiler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
2
|
+
import path, { parse } from 'path';
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
4
|
import * as acorn from 'acorn';
|
|
5
5
|
|
|
@@ -260,11 +260,11 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
|
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* Bundle all .jux files into a single router-based main.js
|
|
263
|
-
* ✅ MODIFIED: Vendor dependencies locally
|
|
263
|
+
* ✅ MODIFIED: Vendor dependencies locally + User Routes + Smart Import Merging
|
|
264
264
|
*/
|
|
265
265
|
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
266
266
|
const startTime = performance.now();
|
|
267
|
-
const { routePrefix = '' } = options;
|
|
267
|
+
const { routePrefix = '', config } = options;
|
|
268
268
|
|
|
269
269
|
console.log('🔀 Bundling .jux files into router...');
|
|
270
270
|
|
|
@@ -281,15 +281,20 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
281
281
|
};
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
-
|
|
284
|
+
const pages = config?.pages || {};
|
|
285
285
|
|
|
286
286
|
const fileTimings = [];
|
|
287
287
|
const views = [];
|
|
288
288
|
const routes = [];
|
|
289
289
|
const sharedModules = new Map(); // ✅ Map<filePath, exportCode>
|
|
290
|
-
|
|
290
|
+
|
|
291
|
+
// ✅ CHANGE: imports collection now stores context { code, filePath }
|
|
292
|
+
const allImports = [];
|
|
291
293
|
const externalModules = new Set(); // ✅ NEW: Track external modules directly
|
|
292
294
|
|
|
295
|
+
// ✅ MAP for user config routing: Key = relative file path, Value = CleanFunctionName
|
|
296
|
+
const fileToFunction = new Map();
|
|
297
|
+
|
|
293
298
|
for (const juxFile of juxFiles) {
|
|
294
299
|
const fileStartTime = performance.now();
|
|
295
300
|
|
|
@@ -306,6 +311,11 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
306
311
|
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
307
312
|
.join('');
|
|
308
313
|
|
|
314
|
+
// ✅ Store mapping for Config Route Resolution
|
|
315
|
+
// Store both 'path/file.jux' and normalized versions
|
|
316
|
+
fileToFunction.set(relativePath, cleanFunctionName);
|
|
317
|
+
fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
|
|
318
|
+
|
|
309
319
|
const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
|
|
310
320
|
const cleanRoutePath = routePath.replace(/\/+/g, '/');
|
|
311
321
|
|
|
@@ -321,7 +331,12 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
321
331
|
ast.body.forEach(node => {
|
|
322
332
|
if (node.type === 'ImportDeclaration') {
|
|
323
333
|
const importStatement = juxContent.slice(node.start, node.end);
|
|
324
|
-
|
|
334
|
+
|
|
335
|
+
// ✅ Store Object with context instead of raw string
|
|
336
|
+
allImports.push({
|
|
337
|
+
code: importStatement,
|
|
338
|
+
filePath: relativePath // relative to projectRoot
|
|
339
|
+
});
|
|
325
340
|
|
|
326
341
|
const moduleName = node.source.value;
|
|
327
342
|
if (!moduleName.startsWith('.') &&
|
|
@@ -359,13 +374,52 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
359
374
|
);
|
|
360
375
|
|
|
361
376
|
views.push(viewFunction);
|
|
362
|
-
|
|
377
|
+
|
|
378
|
+
// Auto-route based on filesystem
|
|
379
|
+
if (config?.defaults?.autoRoute !== false) {
|
|
380
|
+
routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
|
|
381
|
+
}
|
|
363
382
|
|
|
364
383
|
const fileTime = performance.now() - fileStartTime;
|
|
365
384
|
fileTimings.push({ file: relativePath, time: fileTime });
|
|
366
385
|
|
|
367
386
|
const exportNote = hasExports ? ' [+exports]' : '';
|
|
368
|
-
console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
|
|
387
|
+
// console.log(` ✓ ${relativePath} → ${cleanFunctionName}()${exportNote} (${fileTime.toFixed(1)}ms)`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ✅ PROCESS USER CONFIGURED ROUTES
|
|
391
|
+
if (pages) {
|
|
392
|
+
console.log(' 🗺️ Mapping user-configured routes...');
|
|
393
|
+
|
|
394
|
+
const resolveAndAddRoute = (urlPath, targetFile) => {
|
|
395
|
+
// Clean up target file path (remove leading ./ and /)
|
|
396
|
+
const cleanTarget = targetFile.replace(/^(\.\/|\/)/, '');
|
|
397
|
+
const funcName = fileToFunction.get(cleanTarget);
|
|
398
|
+
|
|
399
|
+
if (funcName) {
|
|
400
|
+
// User routes take precedence (prepend to list)
|
|
401
|
+
routes.unshift({ path: urlPath, functionName: funcName });
|
|
402
|
+
console.log(` ➕ ${urlPath} → ${funcName}`);
|
|
403
|
+
} else {
|
|
404
|
+
console.warn(` ⚠️ Route target not found: ${targetFile} (Available keys: ${fileToFunction.size})`);
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
Object.entries(pages).forEach(([key, value]) => {
|
|
409
|
+
if (typeof value === 'string') {
|
|
410
|
+
// Direct mapping: '/' -> './experiments/state.jux'
|
|
411
|
+
resolveAndAddRoute(key, value);
|
|
412
|
+
} else if (typeof value === 'object') {
|
|
413
|
+
// Group mapping
|
|
414
|
+
const prefix = value.prefix || '';
|
|
415
|
+
const groupRoutes = value.routes || {};
|
|
416
|
+
|
|
417
|
+
Object.entries(groupRoutes).forEach(([routePath, targetFile]) => {
|
|
418
|
+
const fullPath = (prefix + routePath).replace(/\/+/g, '/');
|
|
419
|
+
resolveAndAddRoute(fullPath, targetFile);
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
});
|
|
369
423
|
}
|
|
370
424
|
|
|
371
425
|
// ✅ Show slowest files if any took >50ms
|
|
@@ -386,7 +440,7 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
386
440
|
|
|
387
441
|
const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
|
|
388
442
|
|
|
389
|
-
const routerCode = generateRouterBundle(views, routes, sharedModules, allImports);
|
|
443
|
+
const routerCode = generateRouterBundle(views, routes, sharedModules, allImports, projectRoot);
|
|
390
444
|
const bundleGenTime = performance.now() - bundleStartTime;
|
|
391
445
|
|
|
392
446
|
const mainJsFilename = 'main.js';
|
|
@@ -578,6 +632,8 @@ function transformJuxToViewFunction(juxContent, functionName, pageName, relative
|
|
|
578
632
|
});
|
|
579
633
|
}
|
|
580
634
|
|
|
635
|
+
// ✅ CRITICAL FIX: Remove ALL imports so they don't break function scope
|
|
636
|
+
// They are hoisted by generateRouterBundle
|
|
581
637
|
nodesToRemove.push({ start: node.start, end: node.end });
|
|
582
638
|
}
|
|
583
639
|
|
|
@@ -652,35 +708,111 @@ function resolveImportPath(importPath, currentFilePath) {
|
|
|
652
708
|
|
|
653
709
|
/**
|
|
654
710
|
* Generate complete router bundle
|
|
711
|
+
* ✅ IMPROVED: Context-Aware Import Resolution (resolves ../paths before dedupe)
|
|
655
712
|
*/
|
|
656
|
-
function generateRouterBundle(views, routes, sharedModules = new Map(), allImports =
|
|
657
|
-
const libImport = '
|
|
713
|
+
function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = [], projectRoot = '') {
|
|
714
|
+
const libImport = '/lib/jux.js'; // ✅ Use absolute path for core lib
|
|
658
715
|
|
|
659
|
-
//
|
|
660
|
-
const
|
|
661
|
-
.filter(imp => {
|
|
662
|
-
const impStr = imp.trim();
|
|
716
|
+
// Map<NormalizedSource, { defaults: Set<string>, named: Set<string>, namespace: string|null }>
|
|
717
|
+
const mergedImports = new Map();
|
|
663
718
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
719
|
+
// Helper to resolve relative paths to a canonical string
|
|
720
|
+
const getCanonicalSource = (source, contextFilePath) => {
|
|
721
|
+
// 1. Keep externals/aliases/css/http as-is
|
|
722
|
+
if (!source.startsWith('.')) return source;
|
|
723
|
+
if (source.includes('.jux')) return source; // Skipped later anyway
|
|
668
724
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
725
|
+
// 2. Resolve relative paths against the file they came from
|
|
726
|
+
// contextFilePath is relative to projectRoot (e.g. 'experiments/list.jux')
|
|
727
|
+
const dir = path.dirname(contextFilePath);
|
|
673
728
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
729
|
+
// joinedPath is relative to projectRoot (e.g. '../lib/componentsv2')
|
|
730
|
+
const joinedPath = path.join(dir, source);
|
|
731
|
+
|
|
732
|
+
// 3. Normalize to alias if it points to lib
|
|
733
|
+
// Matches 'lib/componentsv2' or '../lib/componentsv2' variants
|
|
734
|
+
if (joinedPath.includes('lib/componentsv2')) {
|
|
735
|
+
return 'juxscript/componentsv2';
|
|
736
|
+
}
|
|
737
|
+
if (joinedPath.includes('lib/components')) {
|
|
738
|
+
return 'juxscript/components';
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// 4. Fallback: return as-is (but normalized separators)
|
|
742
|
+
return joinedPath.replace(/\\/g, '/');
|
|
743
|
+
};
|
|
744
|
+
|
|
745
|
+
const importList = Array.isArray(allImports) ? allImports : Array.from(allImports);
|
|
746
|
+
|
|
747
|
+
for (const item of importList) {
|
|
748
|
+
const code = typeof item === 'string' ? item : item.code;
|
|
749
|
+
const filePath = typeof item === 'string' ? '' : item.filePath; // Context file path
|
|
750
|
+
|
|
751
|
+
try {
|
|
752
|
+
const ast = acorn.parse(code.trim(), { ecmaVersion: 'latest', sourceType: 'module' });
|
|
753
|
+
const node = ast.body[0];
|
|
754
|
+
|
|
755
|
+
if (node && node.type === 'ImportDeclaration') {
|
|
756
|
+
const rawSource = node.source.value;
|
|
757
|
+
|
|
758
|
+
// Skip base libs handled manually
|
|
759
|
+
if (rawSource === 'juxscript' || rawSource === 'juxscript/reactivity') continue;
|
|
760
|
+
if (rawSource.endsWith('.jux')) continue;
|
|
761
|
+
|
|
762
|
+
// ✅ RESOLVE SOURCE USING CONTEXT (Fixes ./lib/.. vs ../../lib/..)
|
|
763
|
+
const source = getCanonicalSource(rawSource, filePath);
|
|
764
|
+
|
|
765
|
+
if (!mergedImports.has(source)) {
|
|
766
|
+
mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
|
|
767
|
+
}
|
|
768
|
+
const storage = mergedImports.get(source);
|
|
769
|
+
|
|
770
|
+
node.specifiers.forEach(spec => {
|
|
771
|
+
if (spec.type === 'ImportDefaultSpecifier') {
|
|
772
|
+
storage.defaults.add(spec.local.name);
|
|
773
|
+
} else if (spec.type === 'ImportSpecifier') {
|
|
774
|
+
if (spec.imported.name !== spec.local.name) {
|
|
775
|
+
storage.named.add(`${spec.imported.name} as ${spec.local.name}`);
|
|
776
|
+
} else {
|
|
777
|
+
storage.named.add(spec.imported.name);
|
|
778
|
+
}
|
|
779
|
+
} else if (spec.type === 'ImportNamespaceSpecifier') {
|
|
780
|
+
storage.namespace = spec.local.name;
|
|
781
|
+
}
|
|
782
|
+
});
|
|
677
783
|
}
|
|
784
|
+
} catch (e) {
|
|
785
|
+
console.warn('⚠️ Failed to parse collected import:', code);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
678
788
|
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
789
|
+
// Generate cleaned export list
|
|
790
|
+
const filteredImports = [];
|
|
791
|
+
|
|
792
|
+
mergedImports.forEach((storage, source) => {
|
|
793
|
+
// Namespace imports usually stand alone
|
|
794
|
+
if (storage.namespace) {
|
|
795
|
+
filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
|
|
796
|
+
}
|
|
682
797
|
|
|
683
|
-
|
|
798
|
+
const parts = [];
|
|
799
|
+
if (storage.defaults.size > 0) {
|
|
800
|
+
const arr = Array.from(storage.defaults);
|
|
801
|
+
parts.push(arr[0]);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
if (storage.named.size > 0) {
|
|
805
|
+
parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) {
|
|
809
|
+
filteredImports.push(`import '${source}';`);
|
|
810
|
+
} else if (parts.length > 0) {
|
|
811
|
+
filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
// Generate shared modules section (deduplicated by file)
|
|
684
816
|
const sharedModulesCode = Array.from(sharedModules.values())
|
|
685
817
|
.filter(code => code.trim())
|
|
686
818
|
.join('\n\n');
|
|
@@ -807,9 +939,9 @@ render();
|
|
|
807
939
|
|
|
808
940
|
/**
|
|
809
941
|
* Generate a unified index.html for router bundle
|
|
810
|
-
* ✅ MODIFIED: Use vendored paths instead of CDN
|
|
942
|
+
* ✅ MODIFIED: Use vendored paths instead of CDN + Hot Reload Script injection
|
|
811
943
|
*/
|
|
812
|
-
export function generateIndexHtml(distDir, bundleResult) {
|
|
944
|
+
export function generateIndexHtml(distDir, bundleResult, options = {}) {
|
|
813
945
|
// ✅ ADD: Defensive check and fallback
|
|
814
946
|
if (!bundleResult) {
|
|
815
947
|
console.warn('⚠️ generateIndexHtml called without bundleResult, using defaults');
|
|
@@ -821,12 +953,36 @@ export function generateIndexHtml(distDir, bundleResult) {
|
|
|
821
953
|
}
|
|
822
954
|
|
|
823
955
|
const { mainJsFilename = 'main.js', routes = [], vendoredPaths = {} } = bundleResult;
|
|
956
|
+
const { isDev = false, wsPort = 3001 } = options;
|
|
824
957
|
|
|
825
958
|
console.log('📄 Generating index.html...');
|
|
826
959
|
|
|
827
960
|
// ✅ Use vendor import map
|
|
828
961
|
const importMapScript = generateVendorImportMap(vendoredPaths);
|
|
829
962
|
|
|
963
|
+
// ✅ Client-side Hot Reload Script
|
|
964
|
+
const hotReloadScript = isDev ? `
|
|
965
|
+
<script>
|
|
966
|
+
(function() {
|
|
967
|
+
console.log('🔌 JUX Hot Reload: Connecting to port ${wsPort}...');
|
|
968
|
+
let ws = new WebSocket('ws://' + window.location.hostname + ':${wsPort}');
|
|
969
|
+
|
|
970
|
+
ws.onopen = () => console.log('✅ JUX Hot Reload: Connected');
|
|
971
|
+
|
|
972
|
+
ws.onmessage = (msg) => {
|
|
973
|
+
try {
|
|
974
|
+
const data = JSON.parse(msg.data);
|
|
975
|
+
if (data.type === 'reload') {
|
|
976
|
+
console.log('🔄 JUX Hot Reload: Refeshing...');
|
|
977
|
+
window.location.reload();
|
|
978
|
+
}
|
|
979
|
+
} catch(e) { console.error('HMR Error:', e); }
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
ws.onclose = () => console.log('❌ JUX Hot Reload: Disconnected');
|
|
983
|
+
})();
|
|
984
|
+
</script>` : '';
|
|
985
|
+
|
|
830
986
|
const html = `<!DOCTYPE html>
|
|
831
987
|
<html lang="en">
|
|
832
988
|
<head>
|
|
@@ -838,6 +994,7 @@ export function generateIndexHtml(distDir, bundleResult) {
|
|
|
838
994
|
<div id="app"></div>
|
|
839
995
|
${importMapScript}
|
|
840
996
|
<script type="module" src="/${mainJsFilename}"></script>
|
|
997
|
+
${hotReloadScript}
|
|
841
998
|
</body>
|
|
842
999
|
</html>`;
|
|
843
1000
|
|
|
@@ -932,15 +1089,21 @@ async function vendorExternalDependencies(externalDeps, distDir) {
|
|
|
932
1089
|
* ✅ NEW: Generate import map with local vendor paths
|
|
933
1090
|
*/
|
|
934
1091
|
function generateVendorImportMap(vendoredPaths) {
|
|
1092
|
+
// ✅ FIX: Use ABSOLUTE paths (start with /) to support nested routes (e.g. /app/dashboard)
|
|
1093
|
+
// When using ./, paths are resolved relative to the current URL, which breaks on sub-pages.
|
|
935
1094
|
const imports = {
|
|
936
|
-
"juxscript": "
|
|
937
|
-
"juxscript/": "
|
|
938
|
-
"juxscript/reactivity": "./lib/reactivity/state.js",
|
|
939
|
-
"juxscript/presets/": "./presets/",
|
|
940
|
-
"juxscript/components/": "./lib/components/",
|
|
1095
|
+
"juxscript/componentsv2": "/lib/componentsv2/index.js", //
|
|
1096
|
+
"juxscript/componentsv2/": "/lib/componentsv2/", //
|
|
941
1097
|
...vendoredPaths // ✅ These are LOCAL paths: "./vendor/axios.js"
|
|
942
1098
|
};
|
|
943
1099
|
|
|
1100
|
+
// Ensure all paths start with / to survive pushState routing
|
|
1101
|
+
Object.keys(imports).forEach(key => {
|
|
1102
|
+
if (imports[key].startsWith('./')) {
|
|
1103
|
+
imports[key] = imports[key].substring(1);
|
|
1104
|
+
}
|
|
1105
|
+
});
|
|
1106
|
+
|
|
944
1107
|
return `<script type="importmap">
|
|
945
1108
|
${JSON.stringify({ imports }, null, 2)}
|
|
946
1109
|
</script>`;
|
package/machinery/config.js
CHANGED
|
@@ -23,12 +23,12 @@ export const defaultConfig = {
|
|
|
23
23
|
// Load user configuration
|
|
24
24
|
export async function loadConfig(projectRoot) {
|
|
25
25
|
const configPath = path.join(projectRoot, 'juxconfig.js');
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
if (fs.existsSync(configPath)) {
|
|
28
28
|
try {
|
|
29
29
|
const configModule = await import(`file://${configPath}`);
|
|
30
30
|
const userConfig = configModule.default || configModule;
|
|
31
|
-
|
|
31
|
+
|
|
32
32
|
return {
|
|
33
33
|
...defaultConfig,
|
|
34
34
|
...userConfig,
|
|
@@ -41,7 +41,7 @@ export async function loadConfig(projectRoot) {
|
|
|
41
41
|
return defaultConfig;
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
console.log('ℹ️ No juxconfig.js found, using defaults');
|
|
46
46
|
return defaultConfig;
|
|
47
47
|
}
|
|
@@ -51,9 +51,9 @@ export async function runBootstrap(bootstrapFunctions = []) {
|
|
|
51
51
|
if (!Array.isArray(bootstrapFunctions) || bootstrapFunctions.length === 0) {
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
|
-
|
|
54
|
+
|
|
55
55
|
console.log('🚀 Running bootstrap functions...\n');
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
for (const fn of bootstrapFunctions) {
|
|
58
58
|
if (typeof fn === 'function') {
|
|
59
59
|
try {
|
|
@@ -63,6 +63,93 @@ export async function runBootstrap(bootstrapFunctions = []) {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
-
|
|
66
|
+
|
|
67
67
|
console.log('✅ Bootstrap complete\n');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Type helper for JUX Configuration (Identity function for Autocomplete)
|
|
72
|
+
* Used in juxconfig.js to provide Intellisense.
|
|
73
|
+
* @param {import('../types/config').JuxConfig} config
|
|
74
|
+
* @returns {import('../types/config').JuxConfig}
|
|
75
|
+
*/
|
|
76
|
+
export function defineConfig(config) {
|
|
77
|
+
return config;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Normalizes user configuration into the strict format used by the compiler/server.
|
|
82
|
+
* Handles "DX Sugar" like:
|
|
83
|
+
* - Inferring file extensions (.jux, .css)
|
|
84
|
+
* - Mapping 'dist' -> 'distribution'
|
|
85
|
+
* - Resolving shortcuts (e.g. layouts)
|
|
86
|
+
* - Defaulting ports
|
|
87
|
+
*/
|
|
88
|
+
export function resolveConfig(userConfig, projectRoot = process.cwd()) {
|
|
89
|
+
const root = userConfig.root || projectRoot;
|
|
90
|
+
|
|
91
|
+
// 1. Directories (Map 'dist' alias to 'distribution')
|
|
92
|
+
const dirs = userConfig.directories || {};
|
|
93
|
+
const directories = {
|
|
94
|
+
source: dirs.source || 'jux',
|
|
95
|
+
distribution: dirs.dist || dirs.distribution || '.jux-dist',
|
|
96
|
+
themes: dirs.themes || 'themes',
|
|
97
|
+
layouts: dirs.layouts || 'themes/layouts',
|
|
98
|
+
assets: dirs.assets || 'themes/assets'
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// 2. Defaults (Infer extensions)
|
|
102
|
+
const defs = userConfig.defaults || {};
|
|
103
|
+
const httpPort = defs.port || defs.httpPort || 3000;
|
|
104
|
+
|
|
105
|
+
const defaults = {
|
|
106
|
+
httpPort,
|
|
107
|
+
wsPort: defs.wsPort || (httpPort + 1),
|
|
108
|
+
autoRoute: defs.autoRoute !== false, // default true
|
|
109
|
+
layout: ensureExt(defs.layout, '.jux') || 'base.jux',
|
|
110
|
+
theme: ensureExt(defs.theme, '.css') || 'base.css'
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// 3. Pages Normalization (Recursively fix route paths)
|
|
114
|
+
const pages = normalizePages(userConfig.pages || {});
|
|
115
|
+
|
|
116
|
+
// 4. Return Normalized Config
|
|
117
|
+
return {
|
|
118
|
+
root,
|
|
119
|
+
directories,
|
|
120
|
+
defaults,
|
|
121
|
+
pages,
|
|
122
|
+
hooks: userConfig.hooks || {}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Helper: Appends extension if missing
|
|
128
|
+
*/
|
|
129
|
+
function ensureExt(file, ext) {
|
|
130
|
+
if (!file) return file;
|
|
131
|
+
return file.endsWith(ext) ? file : `${file}${ext}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Helper: Recursively normalizes page routes
|
|
136
|
+
*/
|
|
137
|
+
function normalizePages(pages) {
|
|
138
|
+
const normalized = {};
|
|
139
|
+
for (const [key, value] of Object.entries(pages)) {
|
|
140
|
+
if (typeof value === 'string') {
|
|
141
|
+
// Direct route: '/' -> 'experiments/index'
|
|
142
|
+
// Becomes: '/' -> 'experiments/index.jux'
|
|
143
|
+
normalized[key] = ensureExt(value, '.jux');
|
|
144
|
+
} else if (typeof value === 'object') {
|
|
145
|
+
// Route Group
|
|
146
|
+
normalized[key] = {
|
|
147
|
+
...value,
|
|
148
|
+
layout: value.layout ? ensureExt(value.layout, '.jux') : undefined,
|
|
149
|
+
theme: value.theme ? ensureExt(value.theme, '.css') : undefined,
|
|
150
|
+
routes: normalizePages(value.routes || {})
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return normalized;
|
|
68
155
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { bundleJuxFilesToRouter, generateIndexHtml } from './compiler.js';
|
|
5
|
+
import { verifyStaticBuild } from './verifier.js';
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const FIXTURES_DIR = path.join(__dirname, 'fixtures');
|
|
9
|
+
const DIST_DIR = path.join(__dirname, '.temp_dist_test');
|
|
10
|
+
|
|
11
|
+
// Setup temp dir
|
|
12
|
+
if (!fs.existsSync(DIST_DIR)) fs.mkdirSync(DIST_DIR);
|
|
13
|
+
|
|
14
|
+
async function runTest(fileName, description) {
|
|
15
|
+
console.log(`\n🧪 TEST: ${description} (${fileName})`);
|
|
16
|
+
console.log(' Context: Trying to build a broken file...');
|
|
17
|
+
|
|
18
|
+
const srcFile = path.join(FIXTURES_DIR, fileName);
|
|
19
|
+
|
|
20
|
+
// Mock finding files by explicitly passing our bad file as the "source"
|
|
21
|
+
// Since bundleJuxFilesToRouter takes a directory, we need to create a temp source dir
|
|
22
|
+
const tempSrcDir = path.join(__dirname, '.temp_src_' + Date.now());
|
|
23
|
+
fs.mkdirSync(tempSrcDir);
|
|
24
|
+
fs.copyFileSync(srcFile, path.join(tempSrcDir, 'index.jux'));
|
|
25
|
+
|
|
26
|
+
let buildSuccess = false;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
// 1. Attempt Bundle
|
|
30
|
+
const result = await bundleJuxFilesToRouter(tempSrcDir, DIST_DIR, { routePrefix: '' });
|
|
31
|
+
|
|
32
|
+
// 2. Generate HTML (needed for verifier to check import map)
|
|
33
|
+
generateIndexHtml(DIST_DIR, result);
|
|
34
|
+
|
|
35
|
+
// 3. Run Verifier
|
|
36
|
+
const verified = verifyStaticBuild(DIST_DIR);
|
|
37
|
+
|
|
38
|
+
if (!verified) {
|
|
39
|
+
console.log(' ✅ SUCCESS: Verifier correctly caught the error!');
|
|
40
|
+
buildSuccess = false;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(' ❌ FAILURE: Build passed but should have failed verification.');
|
|
43
|
+
buildSuccess = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
} catch (e) {
|
|
47
|
+
console.log(` ✅ SUCCESS: Compiler threw error as expected:`);
|
|
48
|
+
console.log(` "${e.message.split('\n')[0]}"`);
|
|
49
|
+
buildSuccess = false;
|
|
50
|
+
} finally {
|
|
51
|
+
// Cleanup
|
|
52
|
+
fs.rmSync(tempSrcDir, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return !buildSuccess;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function main() {
|
|
59
|
+
console.log('🩺 JUX FAILURE DIAGNOSTICS\n');
|
|
60
|
+
|
|
61
|
+
// Test 1: bad_syntax.jux (Should throw during compile)
|
|
62
|
+
await runTest('bad_syntax.jux', 'Syntax Error Detection');
|
|
63
|
+
|
|
64
|
+
// Test 2: ghost_dep.jux (Should pass compile, fail verification)
|
|
65
|
+
await runTest('ghost_dep.jux', 'Ghost Dependency Detection');
|
|
66
|
+
|
|
67
|
+
// Cleanup Dist
|
|
68
|
+
fs.rmSync(DIST_DIR, { recursive: true, force: true });
|
|
69
|
+
console.log('\n🏁 Diagnostics Complete');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main();
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# JUX Module Pattern
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
A `.jux` file can export a component-like interface with:
|
|
5
|
+
- Configuration options
|
|
6
|
+
- Public API methods
|
|
7
|
+
- Event emissions (EDR pattern)
|
|
8
|
+
- Internal/shared state
|
|
9
|
+
- Props (getters)
|
|
10
|
+
|
|
11
|
+
## Pattern Structure
|
|
12
|
+
|
|
13
|
+
```javascript
|
|
14
|
+
// calendar.jux
|
|
15
|
+
import { jux, state } from 'juxscript';
|
|
16
|
+
|
|
17
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
18
|
+
// MODULE DEFINITION
|
|
19
|
+
// ═══════════════════════════════════════════════════════════════════
|
|
20
|
+
|
|
21
|
+
export default function Calendar(id, options = {}) {
|
|
22
|
+
// Internal state
|
|
23
|
+
const today = new Date();
|
|
24
|
+
const currentMonth = state(options.initialMonth ?? today.getMonth());
|
|
25
|
+
const currentYear = state(options.initialYear ?? today.getFullYear());
|
|
26
|
+
const selectedDate = state(null);
|
|
27
|
+
|
|
28
|
+
// Event emitters (EDR pattern)
|
|
29
|
+
const events = {
|
|
30
|
+
dateSelect: [],
|
|
31
|
+
monthChange: [],
|
|
32
|
+
yearChange: []
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Emit helper
|
|
36
|
+
const emit = (eventName, data) => {
|
|
37
|
+
events[eventName]?.forEach(handler => handler(data));
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Public API
|
|
41
|
+
const api = {
|
|
42
|
+
// Props (getters)
|
|
43
|
+
get currentMonth() { return currentMonth.value; },
|
|
44
|
+
get currentYear() { return currentYear.value; },
|
|
45
|
+
get selectedDate() { return selectedDate.value; },
|
|
46
|
+
|
|
47
|
+
// Methods
|
|
48
|
+
nextMonth() {
|
|
49
|
+
let month = currentMonth.value + 1;
|
|
50
|
+
let year = currentYear.value;
|
|
51
|
+
if (month > 11) { month = 0; year++; }
|
|
52
|
+
currentMonth.set(month);
|
|
53
|
+
currentYear.set(year);
|
|
54
|
+
emit('monthChange', { month, year });
|
|
55
|
+
return api;
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
prevMonth() {
|
|
59
|
+
let month = currentMonth.value - 1;
|
|
60
|
+
let year = currentYear.value;
|
|
61
|
+
if (month < 0) { month = 11; year--; }
|
|
62
|
+
currentMonth.set(month);
|
|
63
|
+
currentYear.set(year);
|
|
64
|
+
emit('monthChange', { month, year });
|
|
65
|
+
return api;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
selectDate(date) {
|
|
69
|
+
selectedDate.set(date);
|
|
70
|
+
emit('dateSelect', { date });
|
|
71
|
+
return api;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// Event binding (EDR consumer registration)
|
|
75
|
+
on(eventName, handler) {
|
|
76
|
+
if (events[eventName]) {
|
|
77
|
+
events[eventName].push(handler);
|
|
78
|
+
}
|
|
79
|
+
return api;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// Render
|
|
83
|
+
render(targetId) {
|
|
84
|
+
// Build UI components...
|
|
85
|
+
return api;
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
// Expose for chaining
|
|
89
|
+
id
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
return api;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
```javascript
|
|
99
|
+
// app.jux
|
|
100
|
+
import Calendar from './calendar.jux';
|
|
101
|
+
|
|
102
|
+
const cal = Calendar('my-calendar', {
|
|
103
|
+
initialMonth: 11,
|
|
104
|
+
initialYear: 2024
|
|
105
|
+
})
|
|
106
|
+
.on('dateSelect', ({ date }) => {
|
|
107
|
+
console.log('Date selected:', date);
|
|
108
|
+
})
|
|
109
|
+
.on('monthChange', ({ month, year }) => {
|
|
110
|
+
console.log('Month changed:', month, year);
|
|
111
|
+
})
|
|
112
|
+
.render('#app');
|
|
113
|
+
|
|
114
|
+
// Public API
|
|
115
|
+
cal.nextMonth();
|
|
116
|
+
cal.prevMonth();
|
|
117
|
+
console.log(cal.currentMonth); // getter
|
|
118
|
+
```
|