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.
@@ -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
- return;
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
- const importMatches = juxContent.match(/import\s+.*?from\s+['"][^'"]+['"]\s*;?/g);
331
- if (importMatches) {
332
- importMatches.forEach(imp => allImports.add(imp));
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 the filename so index.html can reference it
401
- return mainJsFilename;
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, routes, mainJsFilename = 'main.js') {
801
- console.log('📄 Generating index.html...');
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
- // Generate navigation links
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
- const importMapScript = generateImportMapScript();
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
+ }
@@ -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.60",
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
  }