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.
- package/machinery/compiler.js +145 -29
- package/package.json +1 -1
package/machinery/compiler.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
|
167
|
-
const
|
|
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
|
-
|
|
173
|
-
|
|
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(
|
|
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 =
|
|
184
|
-
allImports.push({ code: importStatement, filePath: relativePath });
|
|
204
|
+
const importStatement = fileContent.slice(node.start, node.end);
|
|
185
205
|
const moduleName = node.source.value;
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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(
|
|
234
|
+
const exportCode = extractSharedModule(fileContent, rawFunctionName, relativePath);
|
|
199
235
|
if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// ✅
|
|
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({}, '',
|
|
532
|
+
history.pushState({}, '', a.href);
|
|
417
533
|
render();
|
|
418
534
|
});
|
|
419
535
|
window.addEventListener('popstate', render);
|