juxscript 1.0.60 → 1.0.61
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/README.md +32 -1
- package/bin/cli.js +6 -28
- package/lib/components/chart.ts +231 -0
- package/machinery/ast.js +347 -0
- package/machinery/bundleAssets.js +0 -0
- package/machinery/bundleJux.js +0 -0
- package/machinery/bundleVendors.js +0 -0
- package/machinery/compiler.js +163 -46
- package/machinery/server.js +16 -2
- package/machinery/ts-shim.js +46 -0
- package/package.json +4 -1
- package/lib/components/docs-data.json +0 -1958
package/machinery/compiler.js
CHANGED
|
@@ -3,23 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import esbuild from 'esbuild';
|
|
4
4
|
import * as acorn from 'acorn';
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Generate import map script tag
|
|
8
|
-
*/
|
|
9
|
-
function generateImportMapScript() {
|
|
10
|
-
return `<script type="importmap">
|
|
11
|
-
{
|
|
12
|
-
"imports": {
|
|
13
|
-
"juxscript": "./lib/jux.js",
|
|
14
|
-
"juxscript/": "./lib/",
|
|
15
|
-
"juxscript/reactivity": "./lib/reactivity/state.js",
|
|
16
|
-
"juxscript/presets/": "./presets/",
|
|
17
|
-
"juxscript/components/": "./lib/components/"
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
</script>`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
6
|
/**
|
|
24
7
|
* Copy and build the JUX library from TypeScript to JavaScript
|
|
25
8
|
*
|
|
@@ -277,12 +260,7 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
|
|
|
277
260
|
|
|
278
261
|
/**
|
|
279
262
|
* Bundle all .jux files into a single router-based main.js
|
|
280
|
-
*
|
|
281
|
-
* @param {string} projectRoot - Source directory (jux/)
|
|
282
|
-
* @param {string} distDir - Destination directory (jux-dist/)
|
|
283
|
-
* @param {Object} options - Bundle options
|
|
284
|
-
* @param {string} options.routePrefix - Route prefix (e.g., '/experiments')
|
|
285
|
-
* @returns {Promise<string>} - Returns the generated filename (e.g., 'main.1234567890.js')
|
|
263
|
+
* ✅ MODIFIED: Vendor dependencies locally
|
|
286
264
|
*/
|
|
287
265
|
export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
|
|
288
266
|
const startTime = performance.now();
|
|
@@ -294,7 +272,13 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
294
272
|
|
|
295
273
|
if (juxFiles.length === 0) {
|
|
296
274
|
console.log(' No .jux files found');
|
|
297
|
-
|
|
275
|
+
// ✅ FIX: Return empty result instead of undefined
|
|
276
|
+
return {
|
|
277
|
+
mainJsFilename: 'main.js',
|
|
278
|
+
routes: [],
|
|
279
|
+
external: new Set(),
|
|
280
|
+
vendoredPaths: {}
|
|
281
|
+
};
|
|
298
282
|
}
|
|
299
283
|
|
|
300
284
|
console.log(` Found ${juxFiles.length} .jux file(s)`);
|
|
@@ -304,6 +288,7 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
304
288
|
const routes = [];
|
|
305
289
|
const sharedModules = new Map(); // ✅ Map<filePath, exportCode>
|
|
306
290
|
const allImports = new Set();
|
|
291
|
+
const externalModules = new Set(); // ✅ NEW: Track external modules directly
|
|
307
292
|
|
|
308
293
|
for (const juxFile of juxFiles) {
|
|
309
294
|
const fileStartTime = performance.now();
|
|
@@ -326,10 +311,29 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
326
311
|
|
|
327
312
|
const juxContent = fs.readFileSync(juxFile, 'utf-8');
|
|
328
313
|
|
|
329
|
-
// Extract imports
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
314
|
+
// ✅ FIX: Extract imports AND detect external modules in one pass
|
|
315
|
+
try {
|
|
316
|
+
const ast = acorn.parse(juxContent, {
|
|
317
|
+
ecmaVersion: 'latest',
|
|
318
|
+
sourceType: 'module'
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
ast.body.forEach(node => {
|
|
322
|
+
if (node.type === 'ImportDeclaration') {
|
|
323
|
+
const importStatement = juxContent.slice(node.start, node.end);
|
|
324
|
+
allImports.add(importStatement);
|
|
325
|
+
|
|
326
|
+
const moduleName = node.source.value;
|
|
327
|
+
if (!moduleName.startsWith('.') &&
|
|
328
|
+
!moduleName.startsWith('/') &&
|
|
329
|
+
!moduleName.startsWith('http') &&
|
|
330
|
+
!moduleName.startsWith('juxscript')) {
|
|
331
|
+
externalModules.add(moduleName);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
} catch (parseErr) {
|
|
336
|
+
console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
|
|
333
337
|
}
|
|
334
338
|
|
|
335
339
|
// Check if file has exports
|
|
@@ -374,14 +378,20 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
374
378
|
}
|
|
375
379
|
|
|
376
380
|
const bundleStartTime = performance.now();
|
|
381
|
+
|
|
382
|
+
// ✅ CHANGE: Single summary instead of verbose list
|
|
383
|
+
if (externalModules.size > 0) {
|
|
384
|
+
console.log(`\n 📦 External dependencies: ${Array.from(externalModules).join(', ')}`);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
|
|
388
|
+
|
|
377
389
|
const routerCode = generateRouterBundle(views, routes, sharedModules, allImports);
|
|
378
390
|
const bundleGenTime = performance.now() - bundleStartTime;
|
|
379
391
|
|
|
380
|
-
// ✅ Use fixed filename (no timestamp)
|
|
381
392
|
const mainJsFilename = 'main.js';
|
|
382
393
|
const mainJsPath = path.join(distDir, mainJsFilename);
|
|
383
394
|
|
|
384
|
-
// Write to dist/main.js
|
|
385
395
|
const writeStartTime = performance.now();
|
|
386
396
|
fs.writeFileSync(mainJsPath, routerCode);
|
|
387
397
|
const writeTime = performance.now() - writeStartTime;
|
|
@@ -391,14 +401,19 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
|
|
|
391
401
|
console.log(` ✓ Generated: ${path.relative(projectRoot, mainJsPath)}`);
|
|
392
402
|
console.log(`\n 📊 Bundle Statistics:`);
|
|
393
403
|
console.log(` Files processed: ${juxFiles.length}`);
|
|
404
|
+
console.log(` External deps: ${externalModules.size}`);
|
|
405
|
+
console.log(` Vendored locally: ${Object.keys(vendoredPaths).length}`);
|
|
394
406
|
console.log(` Bundle size: ${(routerCode.length / 1024).toFixed(1)} KB`);
|
|
395
|
-
console.log(` Code generation: ${bundleGenTime.toFixed(0)}ms`);
|
|
396
|
-
console.log(` File write: ${writeTime.toFixed(0)}ms`);
|
|
397
407
|
console.log(` Total bundle time: ${totalTime.toFixed(0)}ms`);
|
|
398
408
|
console.log('✅ Router bundle complete\n');
|
|
399
409
|
|
|
400
|
-
// ✅ Return
|
|
401
|
-
return
|
|
410
|
+
// ✅ Return bundleResult with vendor info
|
|
411
|
+
return {
|
|
412
|
+
mainJsFilename,
|
|
413
|
+
routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
|
|
414
|
+
external: externalModules, // ✅ FIX: Use externalModules instead of external
|
|
415
|
+
vendoredPaths
|
|
416
|
+
};
|
|
402
417
|
}
|
|
403
418
|
|
|
404
419
|
/**
|
|
@@ -792,20 +807,25 @@ render();
|
|
|
792
807
|
|
|
793
808
|
/**
|
|
794
809
|
* Generate a unified index.html for router bundle
|
|
795
|
-
*
|
|
796
|
-
* @param {string} distDir - Destination directory
|
|
797
|
-
* @param {Array<{path: string, functionName: string}>} routes - Route definitions
|
|
798
|
-
* @param {string} mainJsFilename - The generated main.js filename (e.g., 'main.1234567890.js')
|
|
810
|
+
* ✅ MODIFIED: Use vendored paths instead of CDN
|
|
799
811
|
*/
|
|
800
|
-
export function generateIndexHtml(distDir,
|
|
801
|
-
|
|
812
|
+
export function generateIndexHtml(distDir, bundleResult) {
|
|
813
|
+
// ✅ ADD: Defensive check and fallback
|
|
814
|
+
if (!bundleResult) {
|
|
815
|
+
console.warn('⚠️ generateIndexHtml called without bundleResult, using defaults');
|
|
816
|
+
bundleResult = {
|
|
817
|
+
mainJsFilename: 'main.js',
|
|
818
|
+
routes: [],
|
|
819
|
+
vendoredPaths: {}
|
|
820
|
+
};
|
|
821
|
+
}
|
|
802
822
|
|
|
803
|
-
|
|
804
|
-
const navLinks = routes
|
|
805
|
-
.map(r => ` <a href="${r.path}">${r.functionName.replace(/_/g, ' ')}</a>`)
|
|
806
|
-
.join(' |\n');
|
|
823
|
+
const { mainJsFilename = 'main.js', routes = [], vendoredPaths = {} } = bundleResult;
|
|
807
824
|
|
|
808
|
-
|
|
825
|
+
console.log('📄 Generating index.html...');
|
|
826
|
+
|
|
827
|
+
// ✅ Use vendor import map
|
|
828
|
+
const importMapScript = generateVendorImportMap(vendoredPaths);
|
|
809
829
|
|
|
810
830
|
const html = `<!DOCTYPE html>
|
|
811
831
|
<html lang="en">
|
|
@@ -815,7 +835,6 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
|
815
835
|
<title>Jux Application</title>
|
|
816
836
|
</head>
|
|
817
837
|
<body data-theme="">
|
|
818
|
-
<!-- App container - router renders here -->
|
|
819
838
|
<div id="app"></div>
|
|
820
839
|
${importMapScript}
|
|
821
840
|
<script type="module" src="/${mainJsFilename}"></script>
|
|
@@ -828,3 +847,101 @@ export function generateIndexHtml(distDir, routes, mainJsFilename = 'main.js') {
|
|
|
828
847
|
console.log(` ✓ Generated: index.html (references ${mainJsFilename})`);
|
|
829
848
|
console.log('✅ Index HTML complete\n');
|
|
830
849
|
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* ✅ NEW: Download and vendor external dependencies locally
|
|
853
|
+
* Copies dependencies from node_modules to .jux-dist/vendor/
|
|
854
|
+
* NO CDN - everything is local, zero hardcoded mappings
|
|
855
|
+
* BUNDLES dependencies to resolve internal imports
|
|
856
|
+
*/
|
|
857
|
+
async function vendorExternalDependencies(externalDeps, distDir) {
|
|
858
|
+
const vendorDir = path.join(distDir, 'vendor');
|
|
859
|
+
|
|
860
|
+
if (!fs.existsSync(vendorDir)) {
|
|
861
|
+
fs.mkdirSync(vendorDir, { recursive: true });
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
const vendoredPaths = {};
|
|
865
|
+
|
|
866
|
+
for (const dep of externalDeps) {
|
|
867
|
+
try {
|
|
868
|
+
const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
|
|
869
|
+
|
|
870
|
+
if (!fs.existsSync(nodeModulePath)) {
|
|
871
|
+
console.warn(` ⚠️ ${dep} not found in node_modules`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const packageJsonPath = path.join(nodeModulePath, 'package.json');
|
|
876
|
+
|
|
877
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
878
|
+
console.warn(` ⚠️ ${dep}: package.json not found`);
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const packageJson = JSON.parse(
|
|
883
|
+
fs.readFileSync(packageJsonPath, 'utf-8')
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const entryPoints = [
|
|
887
|
+
packageJson.module,
|
|
888
|
+
packageJson.browser,
|
|
889
|
+
packageJson.main,
|
|
890
|
+
'index.js'
|
|
891
|
+
].filter(Boolean);
|
|
892
|
+
|
|
893
|
+
let entryFile = null;
|
|
894
|
+
for (const entry of entryPoints) {
|
|
895
|
+
const candidatePath = path.join(nodeModulePath, entry);
|
|
896
|
+
if (fs.existsSync(candidatePath)) {
|
|
897
|
+
entryFile = candidatePath;
|
|
898
|
+
break;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (!entryFile) {
|
|
903
|
+
console.warn(` ⚠️ ${dep}: no valid entry point`);
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const vendorFile = path.join(vendorDir, `${dep}.js`);
|
|
908
|
+
|
|
909
|
+
await esbuild.build({
|
|
910
|
+
entryPoints: [entryFile],
|
|
911
|
+
bundle: true,
|
|
912
|
+
format: 'esm',
|
|
913
|
+
outfile: vendorFile,
|
|
914
|
+
platform: 'browser',
|
|
915
|
+
target: 'es2020',
|
|
916
|
+
minify: false,
|
|
917
|
+
logLevel: 'warning'
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
vendoredPaths[dep] = `/vendor/${dep}.js`;
|
|
921
|
+
console.log(` ✓ ${dep} → vendor/${dep}.js`);
|
|
922
|
+
|
|
923
|
+
} catch (err) {
|
|
924
|
+
console.error(` ❌ ${dep}: ${err.message}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
return vendoredPaths;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* ✅ NEW: Generate import map with local vendor paths
|
|
933
|
+
*/
|
|
934
|
+
function generateVendorImportMap(vendoredPaths) {
|
|
935
|
+
const imports = {
|
|
936
|
+
"juxscript": "./lib/jux.js",
|
|
937
|
+
"juxscript/": "./lib/",
|
|
938
|
+
"juxscript/reactivity": "./lib/reactivity/state.js",
|
|
939
|
+
"juxscript/presets/": "./presets/",
|
|
940
|
+
"juxscript/components/": "./lib/components/",
|
|
941
|
+
...vendoredPaths // ✅ These are LOCAL paths: "./vendor/axios.js"
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
return `<script type="importmap">
|
|
945
|
+
${JSON.stringify({ imports }, null, 2)}
|
|
946
|
+
</script>`;
|
|
947
|
+
}
|
package/machinery/server.js
CHANGED
|
@@ -69,11 +69,25 @@ async function serve(httpPort = 3000, wsPort = 3001, distDir = './.jux-dist') {
|
|
|
69
69
|
next();
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
// ✅ ADD: Explicit MIME type for JavaScript files
|
|
73
|
+
app.use((req, res, next) => {
|
|
74
|
+
if (req.path.endsWith('.js')) {
|
|
75
|
+
res.setHeader('Content-Type', 'application/javascript; charset=utf-8');
|
|
76
|
+
}
|
|
77
|
+
next();
|
|
78
|
+
});
|
|
79
|
+
|
|
72
80
|
// Serve static files
|
|
73
81
|
app.use(express.static(absoluteDistDir));
|
|
74
82
|
|
|
75
|
-
// SPA fallback
|
|
76
|
-
app.use((req, res) => {
|
|
83
|
+
// ✅ FIX: Only apply SPA fallback to routes, not static files
|
|
84
|
+
app.use((req, res, next) => {
|
|
85
|
+
// Skip SPA fallback for file extensions (static assets)
|
|
86
|
+
if (/\.[a-zA-Z0-9]+$/.test(req.path)) {
|
|
87
|
+
return res.status(404).send('File not found');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// SPA fallback for routes only
|
|
77
91
|
if (req.accepts('html')) {
|
|
78
92
|
const indexPath = path.join(absoluteDistDir, 'index.html');
|
|
79
93
|
if (fs.existsSync(indexPath)) {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import esbuild from 'esbuild';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* TypeScript Shim - Strips type annotations using esbuild
|
|
5
|
+
* This is rock-solid and handles ALL TypeScript syntax correctly
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strip TypeScript type annotations from code using esbuild
|
|
10
|
+
*
|
|
11
|
+
* @param {string} code - TypeScript source code
|
|
12
|
+
* @returns {string} - JavaScript code with types removed
|
|
13
|
+
*/
|
|
14
|
+
export function stripTypes(code) {
|
|
15
|
+
try {
|
|
16
|
+
// ✅ Use esbuild to transform TypeScript → JavaScript
|
|
17
|
+
const result = esbuild.transformSync(code, {
|
|
18
|
+
loader: 'ts',
|
|
19
|
+
format: 'esm',
|
|
20
|
+
target: 'es2020'
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return result.code;
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('❌ Failed to strip TypeScript:', err.message);
|
|
26
|
+
// Return original code if transformation fails
|
|
27
|
+
return code;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Check if a file is TypeScript based on extension
|
|
33
|
+
*/
|
|
34
|
+
export function isTypeScript(filePath) {
|
|
35
|
+
return filePath.endsWith('.ts') || filePath.endsWith('.tsx');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Load and parse a file, automatically stripping types if it's TypeScript
|
|
40
|
+
*/
|
|
41
|
+
export function loadAndStripTypes(filePath, fileContent) {
|
|
42
|
+
if (isTypeScript(filePath)) {
|
|
43
|
+
return stripTypes(fileContent);
|
|
44
|
+
}
|
|
45
|
+
return fileContent;
|
|
46
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "juxscript",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.61",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A JavaScript UX authorship platform",
|
|
6
6
|
"main": "lib/jux.js",
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
60
|
"acorn": "^8.15.0",
|
|
61
|
+
"axios": "^1.6.0",
|
|
62
|
+
"chart.js": "^4.5.1",
|
|
61
63
|
"chokidar": "^3.5.3",
|
|
62
64
|
"esbuild": "^0.19.0",
|
|
63
65
|
"express": "^4.18.2",
|
|
@@ -68,6 +70,7 @@
|
|
|
68
70
|
"@types/express": "^4.17.17",
|
|
69
71
|
"@types/node": "^20.0.0",
|
|
70
72
|
"@types/ws": "^8.5.5",
|
|
73
|
+
"acorn-walk": "^8.3.4",
|
|
71
74
|
"jsdom": "^27.4.0",
|
|
72
75
|
"typescript": "^5.0.0"
|
|
73
76
|
}
|