juxscript 1.0.71 → 1.0.73

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/compiler.js +145 -29
  2. package/package.json +1 -1
@@ -89,12 +89,15 @@ export async function copyPresetsToOutput(packageRoot, distDir) {
89
89
  }
90
90
 
91
91
  function findFiles(dir, extension, fileList = []) {
92
+ if (!fs.existsSync(dir)) return fileList;
92
93
  const files = fs.readdirSync(dir);
93
94
  files.forEach(file => {
94
95
  const filePath = path.join(dir, file);
95
96
  const stat = fs.statSync(filePath);
96
97
  if (stat.isDirectory()) {
97
- findFiles(filePath, extension, fileList);
98
+ if (file !== 'node_modules' && file !== '.git' && file !== 'jux-dist') {
99
+ findFiles(filePath, extension, fileList);
100
+ }
98
101
  } else if (file.endsWith(extension)) {
99
102
  fileList.push(filePath);
100
103
  }
@@ -149,12 +152,22 @@ function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, exclude
149
152
  */
150
153
  export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
151
154
  const { routePrefix = '', config } = options;
155
+
156
+ // ✅ 1. Find all processable source files (.jux AND .js)
157
+ // This ensures locally imported helper JS files are bundled and namespaced correctly.
152
158
  const juxFiles = findFiles(projectRoot, '.jux');
159
+ const jsFiles = findFiles(projectRoot, '.js');
160
+
161
+ // Combine, filtering duplicates if any
162
+ const allSourceFiles = Array.from(new Set([...juxFiles, ...jsFiles]));
153
163
 
154
- if (juxFiles.length === 0) {
164
+ if (allSourceFiles.length === 0) {
155
165
  return { mainJsFilename: 'main.js', routes: [], external: new Set(), vendoredPaths: {} };
156
166
  }
157
167
 
168
+ // Set of relative paths being bundled (used to decide if an import should be skipped)
169
+ const bundledPaths = new Set(allSourceFiles.map(f => path.relative(projectRoot, f).replace(/\\/g, '/')));
170
+
158
171
  const pages = config?.pages || {};
159
172
  const views = [];
160
173
  const routes = [];
@@ -163,28 +176,50 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
163
176
  const externalModules = new Set();
164
177
  const fileToFunction = new Map();
165
178
 
166
- for (const juxFile of juxFiles) {
167
- const relativePath = path.relative(projectRoot, juxFile);
179
+ for (const sourceFile of allSourceFiles) {
180
+ const isJux = sourceFile.endsWith('.jux');
181
+ const relativePath = path.relative(projectRoot, sourceFile);
168
182
  const parsedPath = path.parse(relativePath);
183
+
184
+ // Normalize function name: experiments/my-data -> ExperimentsMyData
169
185
  const rawFunctionName = parsedPath.dir ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}` : parsedPath.name;
170
186
  const cleanFunctionName = rawFunctionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
171
187
 
172
- fileToFunction.set(relativePath, cleanFunctionName);
173
- fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
188
+ // Only .jux files generate Routes/Views
189
+ if (isJux) {
190
+ fileToFunction.set(relativePath, cleanFunctionName);
191
+ fileToFunction.set(relativePath.split(path.sep).join('/'), cleanFunctionName);
192
+ }
174
193
 
175
194
  const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
176
195
  const cleanRoutePath = routePath.replace(/\/+/g, '/');
177
- const juxContent = fs.readFileSync(juxFile, 'utf-8');
178
196
 
197
+ const fileContent = fs.readFileSync(sourceFile, 'utf-8');
198
+
199
+ // Parse Imports
179
200
  try {
180
- const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
201
+ const ast = acorn.parse(fileContent, { ecmaVersion: 'latest', sourceType: 'module' });
181
202
  ast.body.forEach(node => {
182
203
  if (node.type === 'ImportDeclaration') {
183
- const importStatement = juxContent.slice(node.start, node.end);
184
- allImports.push({ code: importStatement, filePath: relativePath });
204
+ const importStatement = fileContent.slice(node.start, node.end);
185
205
  const moduleName = node.source.value;
186
- if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
187
- externalModules.add(moduleName);
206
+
207
+ // ✅ CHECK: Is this import pointing to a file we are bundling?
208
+ let isBundledLocal = false;
209
+ if (moduleName.startsWith('.')) {
210
+ const resolved = resolveImportPath(moduleName, relativePath);
211
+ if (bundledPaths.has(resolved)) {
212
+ isBundledLocal = true;
213
+ }
214
+ }
215
+
216
+ // ONLY add to global imports if it is NOT a local bundled file
217
+ if (!isBundledLocal) {
218
+ allImports.push({ code: importStatement, filePath: relativePath });
219
+
220
+ if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
221
+ externalModules.add(moduleName);
222
+ }
188
223
  }
189
224
  }
190
225
  });
@@ -192,18 +227,23 @@ export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {})
192
227
  console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
193
228
  }
194
229
 
195
- const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(juxContent);
230
+ // Process Exports -> Shared Modules (Code Hoisting)
231
+ const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(fileContent);
196
232
  if (hasExports) {
197
233
  const exportKey = relativePath;
198
- const exportCode = extractSharedModule(juxContent, rawFunctionName, relativePath);
234
+ const exportCode = extractSharedModule(fileContent, rawFunctionName, relativePath);
199
235
  if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
200
236
  }
201
237
 
202
- const viewFunction = transformJuxToViewFunction(juxContent, rawFunctionName, parsedPath.name, relativePath, sharedModules);
203
- views.push(viewFunction);
238
+ // Process .jux -> View Functions
239
+ if (isJux) {
240
+ // Pass bundledPaths so we know which imports to rewrite
241
+ const viewFunction = transformJuxToViewFunction(fileContent, rawFunctionName, parsedPath.name, relativePath, sharedModules, bundledPaths);
242
+ views.push(viewFunction);
204
243
 
205
- if (config?.defaults?.autoRoute !== false) {
206
- routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
244
+ if (config?.defaults?.autoRoute !== false) {
245
+ routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
246
+ }
207
247
  }
208
248
  }
209
249
 
@@ -265,7 +305,8 @@ function extractSharedModule(juxContent, moduleName, sourceFilePath) {
265
305
  }
266
306
 
267
307
  function namespaceExportedIdentifiers(code, sourceFilePath) {
268
- const namespace = sourceFilePath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
308
+ // FIX: Strip .jux AND .js extensions for stable namespacing
309
+ const namespace = sourceFilePath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
269
310
  try {
270
311
  const ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module' });
271
312
  const identifiers = [];
@@ -285,7 +326,7 @@ function namespaceExportedIdentifiers(code, sourceFilePath) {
285
326
  }
286
327
  }
287
328
 
288
- function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules) {
329
+ function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules, bundledPaths) {
289
330
  let result;
290
331
  try {
291
332
  const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
@@ -294,16 +335,22 @@ function transformJuxToViewFunction(juxContent, functionName, pageName, relative
294
335
  ast.body.forEach(node => {
295
336
  if (node.type === 'ImportDeclaration') {
296
337
  const importPath = node.source.value;
297
- if (importPath.endsWith('.jux')) {
338
+ const resolvedPath = resolveImportPath(importPath, relativePath);
339
+
340
+ // ✅ Check if this import is pointing to a bundled file
341
+ const isBundled = (bundledPaths && bundledPaths.has(resolvedPath)) || importPath.endsWith('.jux');
342
+
343
+ if (isBundled) {
298
344
  node.specifiers.forEach(spec => {
299
345
  if (spec.type === 'ImportSpecifier') {
300
- const resolvedPath = resolveImportPath(importPath, relativePath);
301
- const namespace = resolvedPath.replace(/\.jux$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
346
+ // Apply namespace convention to .js imports too
347
+ const namespace = resolvedPath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
302
348
  importReplacements.set(spec.local.name, `${spec.imported.name}$${namespace}`);
303
349
  }
304
350
  });
351
+ // Remove local imports as they will be inlined in main.js
352
+ nodesToRemove.push({ start: node.start, end: node.end });
305
353
  }
306
- nodesToRemove.push({ start: node.start, end: node.end });
307
354
  }
308
355
  if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
309
356
  nodesToRemove.push({ start: node.start, end: node.end });
@@ -320,7 +367,19 @@ function transformJuxToViewFunction(juxContent, functionName, pageName, relative
320
367
  }
321
368
  result = result.replace(/\.renderTo\(container\)/g, '.render("#app")').replace(/\.render\(\s*\)/g, '.render("#app")');
322
369
  const cleanName = functionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
323
- return `\n// View: ${cleanName}\nfunction ${cleanName}() {\n${result}\n \n return document.getElementById('app');\n}`;
370
+
371
+ // ✅ Wrap View implementation in underscore function, then export Wrapped version
372
+ return `
373
+ // View: ${cleanName}
374
+ function _${cleanName}() {
375
+ ${result}
376
+
377
+ return document.getElementById('app');
378
+ }
379
+ const ${cleanName} = JuxError.wrap('${cleanName}', _${cleanName});
380
+ // Register Source
381
+ JuxError.register('${cleanName}', '${relativePath}');
382
+ `;
324
383
  }
325
384
 
326
385
  function resolveImportPath(importPath, currentFilePath) {
@@ -342,7 +401,7 @@ function generateRouterBundle(views, routes, sharedModules = new Map(), allImpor
342
401
  }
343
402
  if (joinedPath.includes('lib/components')) return 'juxscript/components';
344
403
 
345
- // ✅ FIX: Ensure local paths start with ./ or / to avoid Ghost Dependency errors in Verifier
404
+ // ✅ Fix: Ensure local relative paths start with ./ or /
346
405
  let normalized = joinedPath.replace(/\\/g, '/');
347
406
  if (!normalized.startsWith('.') && !normalized.startsWith('/')) {
348
407
  normalized = './' + normalized;
@@ -359,7 +418,6 @@ function generateRouterBundle(views, routes, sharedModules = new Map(), allImpor
359
418
  const node = ast.body[0];
360
419
  if (node && node.type === 'ImportDeclaration') {
361
420
  const rawSource = node.source.value;
362
- if (rawSource.endsWith('.jux')) continue;
363
421
  const source = getCanonicalSource(rawSource, filePath);
364
422
  if (!mergedImports.has(source)) mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
365
423
  const storage = mergedImports.get(source);
@@ -382,13 +440,71 @@ function generateRouterBundle(views, routes, sharedModules = new Map(), allImpor
382
440
  else if (parts.length > 0) filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
383
441
  });
384
442
 
443
+ const juxDebugUtils = `
444
+ // ============================================
445
+ // JUX DEBUG UTILITIES
446
+ // ============================================
447
+ const JUX_DEBUG = true;
448
+
449
+ const JuxError = {
450
+ sourceMap: {},
451
+
452
+ register(viewName, sourceFile) {
453
+ this.sourceMap[viewName] = sourceFile;
454
+ },
455
+
456
+ wrap(viewName, viewFn) {
457
+ return function() {
458
+ const sourceFile = JuxError.sourceMap[viewName] || 'unknown';
459
+ if (JUX_DEBUG) console.log(\`🚀 [JUX] Rendering: \${viewName} (\${sourceFile})\`);
460
+
461
+ try {
462
+ return viewFn.apply(this, arguments);
463
+ } catch (error) {
464
+ console.error(\`🚨 JUX RUNTIME ERROR\\nView: \${viewName}\\nSource: \${sourceFile}\`);
465
+ console.error(error);
466
+
467
+ const app = document.getElementById('app');
468
+ if (app) {
469
+ app.innerHTML = \`
470
+ <div style="font-family: monospace; background: #1e1e1e; color: #ff6b6b; min-height: 100vh; padding: 2rem;">
471
+ <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem;">
472
+ <div style="font-size: 2rem;">🚨</div>
473
+ <div>
474
+ <h1 style="margin:0; font-size: 1.5rem; color: #ff6b6b;">JUX Runtime Error</h1>
475
+ </div>
476
+ </div>
477
+
478
+ <div style="margin-bottom: 1rem;">
479
+ <div style="color: #888; margin-bottom: 4px;">View:</div>
480
+ <span style="color: #fff; font-weight: bold;">\${viewName}</span>
481
+ </div>
482
+
483
+ <div style="margin-bottom: 2rem;">
484
+ <div style="color: #888; margin-bottom: 4px;">Source:</div>
485
+ <span style="color: #4caf50; font-family: monospace;">\${sourceFile}</span>
486
+ </div>
487
+
488
+ <pre style="background: #2d2d2d; padding: 20px; border-radius: 8px; overflow-x: auto; color: #e0e0e0; border-left: 4px solid #ff6b6b;">\${error.message}\\n\\n\${error.stack}</pre>
489
+ </div>
490
+ \`;
491
+ }
492
+ throw error;
493
+ }
494
+ };
495
+ }
496
+ };
497
+ `;
498
+
385
499
  return `// Generated Jux Router Bundle
386
500
  ${filteredImports.join('\n')}
387
501
 
502
+ ${juxDebugUtils}
503
+
388
504
  // SHARED MODULES
389
505
  ${Array.from(sharedModules.values()).filter(c => c.trim()).join('\n\n')}
390
506
 
391
- // VIEWS
507
+ // VIEWS (Wrapped in JuxError)
392
508
  ${views.filter(v => v.trim()).join('\n\n')}
393
509
 
394
510
  function JuxNotFound() { jux.heading(1, { text: '404 - Page Not Found' }).render('#app'); return document.getElementById('app'); }
@@ -413,7 +529,7 @@ document.addEventListener('click', e => {
413
529
  const a = e.target.closest('a');
414
530
  if (!a || a.dataset.router === 'false' || new URL(a.href).origin !== location.origin) return;
415
531
  e.preventDefault();
416
- history.pushState({}, '', url.pathname);
532
+ history.pushState({}, '', a.href);
417
533
  render();
418
534
  });
419
535
  window.addEventListener('popstate', render);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "juxscript",
3
- "version": "1.0.71",
3
+ "version": "1.0.73",
4
4
  "type": "module",
5
5
  "description": "A JavaScript UX authorship platform",
6
6
  "main": "lib/jux.js",