juxscript 1.0.89 → 1.0.90

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.
@@ -1,706 +0,0 @@
1
- import fs from 'fs';
2
- import path, { parse } from 'path';
3
- import esbuild from 'esbuild';
4
- import * as acorn from 'acorn';
5
-
6
- /**
7
- * Copy and build the JUX library from TypeScript to JavaScript
8
- */
9
- export async function copyLibToOutput(projectRoot, distDir) {
10
- const libSrc = path.resolve(projectRoot, '../lib');
11
- if (!fs.existsSync(libSrc)) {
12
- throw new Error(`lib/ directory not found at ${libSrc}`);
13
- }
14
- const libDest = path.join(distDir, 'lib');
15
-
16
- if (fs.existsSync(libDest)) {
17
- fs.rmSync(libDest, { recursive: true });
18
- }
19
- fs.mkdirSync(libDest, { recursive: true });
20
-
21
- const tsFiles = findFiles(libSrc, '.ts');
22
- if (tsFiles.length === 0) return;
23
-
24
- try {
25
- await esbuild.build({
26
- entryPoints: tsFiles,
27
- bundle: false,
28
- format: 'esm',
29
- outdir: libDest,
30
- outbase: libSrc,
31
- platform: 'browser',
32
- target: 'es2020',
33
- loader: { '.ts': 'ts' },
34
- logLevel: 'error'
35
- });
36
- copyNonTsFiles(libSrc, libDest);
37
- } catch (err) {
38
- console.error('❌ Failed to build TypeScript:', err.message);
39
- throw err;
40
- }
41
- }
42
-
43
- /**
44
- * Copy project assets (CSS, JS, images) from jux/ to dist/
45
- */
46
- export async function copyProjectAssets(projectRoot, distDir) {
47
- const allFiles = [];
48
- findProjectFiles(projectRoot, ['.css', '.js'], allFiles, projectRoot);
49
-
50
- for (const srcPath of allFiles) {
51
- const relativePath = path.relative(projectRoot, srcPath);
52
- const destPath = path.join(distDir, relativePath);
53
- const destDir = path.dirname(destPath);
54
- if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
55
- fs.copyFileSync(srcPath, destPath);
56
- }
57
- }
58
-
59
- /**
60
- * Transpile TypeScript files from jux/ to jux-dist/
61
- */
62
- export async function transpileProjectTypeScript(srcDir, destDir) {
63
- const tsFiles = findFiles(srcDir, '.ts');
64
- if (tsFiles.length === 0) return;
65
-
66
- try {
67
- await esbuild.build({
68
- entryPoints: tsFiles,
69
- bundle: false,
70
- format: 'esm',
71
- outdir: destDir,
72
- outbase: srcDir,
73
- platform: 'browser',
74
- target: 'es2020',
75
- loader: { '.ts': 'ts' },
76
- logLevel: 'error'
77
- });
78
- } catch (err) {
79
- console.error('❌ Failed to transpile TypeScript:', err.message);
80
- throw err;
81
- }
82
- }
83
-
84
- /**
85
- * Copy presets (No-op log silenced)
86
- */
87
- export async function copyPresetsToOutput(packageRoot, distDir) {
88
- // No-op
89
- }
90
-
91
- function findFiles(dir, extension, fileList = []) {
92
- if (!fs.existsSync(dir)) return fileList;
93
- const files = fs.readdirSync(dir);
94
- files.forEach(file => {
95
- const filePath = path.join(dir, file);
96
- const stat = fs.statSync(filePath);
97
- if (stat.isDirectory()) {
98
- if (file !== 'node_modules' && file !== '.git' && file !== 'jux-dist') {
99
- findFiles(filePath, extension, fileList);
100
- }
101
- } else if (file.endsWith(extension)) {
102
- fileList.push(filePath);
103
- }
104
- });
105
- return fileList;
106
- }
107
-
108
- function copyNonTsFiles(src, dest) {
109
- const entries = fs.readdirSync(src, { withFileTypes: true });
110
-
111
- for (const entry of entries) {
112
- const srcPath = path.join(src, entry.name);
113
- const destPath = path.join(dest, entry.name);
114
-
115
- if (entry.isDirectory()) {
116
- if (entry.name === 'layouts') continue;
117
- if (!fs.existsSync(destPath)) fs.mkdirSync(destPath, { recursive: true });
118
- copyNonTsFiles(srcPath, destPath);
119
- } else if (entry.isFile()) {
120
- const ext = path.extname(entry.name);
121
- if (ext === '.js' || ext === '.map') {
122
- let tsSibling = '';
123
- if (ext === '.js') tsSibling = srcPath.replace(/\.js$/, '.ts');
124
- else if (ext === '.map') tsSibling = srcPath.replace(/\.js\.map$/, '.ts');
125
- if (tsSibling && fs.existsSync(tsSibling)) continue;
126
- }
127
- if (['.css', '.json', '.js', '.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) {
128
- fs.copyFileSync(srcPath, destPath);
129
- }
130
- }
131
- }
132
- }
133
-
134
- function findProjectFiles(dir, extensions, fileList = [], rootDir = dir, excludeDirs = ['node_modules', 'jux-dist', '.git', 'lib']) {
135
- if (!fs.existsSync(dir)) return fileList;
136
- const entries = fs.readdirSync(dir, { withFileTypes: true });
137
- for (const entry of entries) {
138
- const fullPath = path.join(dir, entry.name);
139
- if (entry.isDirectory()) {
140
- if (excludeDirs.includes(entry.name)) continue;
141
- findProjectFiles(fullPath, extensions, fileList, rootDir, excludeDirs);
142
- } else {
143
- const hasExtension = extensions.some(ext => entry.name.endsWith(ext));
144
- if (hasExtension) fileList.push(fullPath);
145
- }
146
- }
147
- return fileList;
148
- }
149
-
150
- /**
151
- * Bundle all .jux files into a single router-based main.js
152
- */
153
- export async function bundleJuxFilesToRouter(projectRoot, distDir, options = {}) {
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.
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]));
163
-
164
- if (allSourceFiles.length === 0) {
165
- return { mainJsFilename: 'main.js', routes: [], external: new Set(), vendoredPaths: {} };
166
- }
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
-
171
- const pages = config?.pages || {};
172
- const views = [];
173
- const routes = [];
174
- const sharedModules = new Map();
175
- const allImports = [];
176
- const externalModules = new Set();
177
- const fileToFunction = new Map();
178
-
179
- for (const sourceFile of allSourceFiles) {
180
- const isJux = sourceFile.endsWith('.jux');
181
- const relativePath = path.relative(projectRoot, sourceFile);
182
- const parsedPath = path.parse(relativePath);
183
-
184
- // Normalize function name: experiments/my-data -> ExperimentsMyData
185
- const rawFunctionName = parsedPath.dir ? `${parsedPath.dir.replace(/\//g, '_')}_${parsedPath.name}` : parsedPath.name;
186
- const cleanFunctionName = rawFunctionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
187
-
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
-
193
- // ✅ FIX: Register lookup without extension to match juxconfig routes like "/path/to/view"
194
- const noExt = relativePath.replace(/\.jux$/, '');
195
- const noExtNormalized = noExt.split(path.sep).join('/');
196
- fileToFunction.set(noExt, cleanFunctionName);
197
- fileToFunction.set(noExtNormalized, cleanFunctionName);
198
- }
199
-
200
- const routePath = routePrefix + '/' + (parsedPath.dir ? `${parsedPath.dir}/` : '') + parsedPath.name;
201
- const cleanRoutePath = routePath.replace(/\/+/g, '/');
202
-
203
- const fileContent = fs.readFileSync(sourceFile, 'utf-8');
204
-
205
- // Parse Imports
206
- try {
207
- const ast = acorn.parse(fileContent, { ecmaVersion: 'latest', sourceType: 'module' });
208
- ast.body.forEach(node => {
209
- if (node.type === 'ImportDeclaration') {
210
- const importStatement = fileContent.slice(node.start, node.end);
211
- const moduleName = node.source.value;
212
-
213
- // ✅ CHECK: Is this import pointing to a file we are bundling?
214
- let isBundledLocal = false;
215
- // Support relative (./, ../) OR project-root relative (/) imports
216
- if (moduleName.startsWith('.') || moduleName.startsWith('/')) {
217
- const resolved = resolveImportPath(moduleName, relativePath);
218
-
219
- if (bundledPaths.has(resolved)) {
220
- isBundledLocal = true;
221
- }
222
- }
223
- // ONLY add to global imports if it is NOT a local bundled file
224
- if (!isBundledLocal) {
225
- allImports.push({ code: importStatement, filePath: relativePath });
226
-
227
- if (!moduleName.startsWith('.') && !moduleName.startsWith('/') && !moduleName.startsWith('http') && !moduleName.startsWith('juxscript')) {
228
- externalModules.add(moduleName);
229
- }
230
- }
231
- }
232
- });
233
- } catch (parseErr) {
234
- console.warn(` ⚠️ Failed to parse imports in ${relativePath}, skipping`);
235
- }
236
-
237
- // Process Exports -> Shared Modules (Code Hoisting)
238
- const hasExports = /^\s*export\s+(const|let|function|class|{)/m.test(fileContent);
239
- if (hasExports) {
240
- const exportKey = relativePath;
241
- // ✅ FIX: Pass bundledPaths so imports can be resolved and rewritten in shared code
242
- const exportCode = extractSharedModule(fileContent, rawFunctionName, relativePath, bundledPaths);
243
- if (exportCode.trim()) sharedModules.set(exportKey, exportCode);
244
- }
245
-
246
- // Process .jux -> View Functions
247
- if (isJux) {
248
- // Pass bundledPaths so we know which imports to rewrite
249
- const viewFunction = transformJuxToViewFunction(fileContent, rawFunctionName, parsedPath.name, relativePath, sharedModules, bundledPaths);
250
- views.push(viewFunction);
251
-
252
- if (config?.defaults?.autoRoute !== false) {
253
- routes.push({ path: cleanRoutePath, functionName: cleanFunctionName });
254
- }
255
- }
256
- }
257
-
258
- if (pages) {
259
- const resolveAndAddRoute = (urlPath, targetFile) => {
260
- // ✅ FIX: Handle ./ and / prefixes in config paths
261
- const cleanTarget = targetFile.replace(/^(\.\/|\/)/, '');
262
- let funcName = fileToFunction.get(cleanTarget);
263
-
264
- // Try appending .jux if not found and no extension provided
265
- if (!funcName && !cleanTarget.endsWith('.jux')) {
266
- funcName = fileToFunction.get(cleanTarget + '.jux');
267
- }
268
-
269
- if (funcName) {
270
- console.log(` 📍 Custom Route: ${urlPath.padEnd(25)} -> ${funcName} (${targetFile})`);
271
- routes.unshift({ path: urlPath, functionName: funcName });
272
- } else {
273
- console.warn(` ⚠️ Route target not found: ${targetFile} (Cleaned: ${cleanTarget})`);
274
- }
275
- };
276
- Object.entries(pages).forEach(([key, value]) => {
277
- if (typeof value === 'string') {
278
- resolveAndAddRoute(key, value);
279
- } else if (typeof value === 'object') {
280
- const prefix = value.prefix || '';
281
- const groupRoutes = value.routes || {};
282
- Object.entries(groupRoutes).forEach(([routePath, targetFile]) => {
283
- const fullPath = (prefix + routePath).replace(/\/+/g, '/');
284
- resolveAndAddRoute(fullPath, targetFile);
285
- });
286
- }
287
- });
288
- }
289
-
290
- const vendoredPaths = await vendorExternalDependencies(externalModules, distDir);
291
- const routerCode = generateRouterBundle(views, routes, sharedModules, allImports, projectRoot);
292
- const mainJsFilename = 'main.js';
293
- const mainJsPath = path.join(distDir, mainJsFilename);
294
- fs.writeFileSync(mainJsPath, routerCode);
295
-
296
- return {
297
- mainJsFilename,
298
- routes: routes.map(r => ({ path: r.path, functionName: r.functionName })),
299
- external: externalModules,
300
- vendoredPaths
301
- };
302
- }
303
-
304
- function extractSharedModule(juxContent, moduleName, sourceFilePath, bundledPaths) {
305
- // 1. Calculate Namespace
306
- const namespace = sourceFilePath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
307
-
308
- let ast;
309
- try {
310
- ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
311
- } catch (err) {
312
- throw new Error(`Invalid JavaScript syntax in ${sourceFilePath}: ${err.message}`);
313
- }
314
-
315
- // 2. Identify ALL Identifiers to rename (Local defs + Imports)
316
- // We must rename everything to avoid global collision in the flat bundle
317
- const identifiersToRename = new Map();
318
-
319
- // A. Local Declarations (Self-namespacing)
320
- const addId = (idNode) => {
321
- if (idNode && idNode.type === 'Identifier') {
322
- identifiersToRename.set(idNode.name, `${idNode.name}$${namespace}`);
323
- }
324
- };
325
-
326
- ast.body.forEach(node => {
327
- if (node.type === 'FunctionDeclaration' || node.type === 'ClassDeclaration') {
328
- addId(node.id);
329
- } else if (node.type === 'VariableDeclaration') {
330
- node.declarations.forEach(d => addId(d.id));
331
- } else if ((node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') && node.declaration) {
332
- const d = node.declaration;
333
- if (d.type === 'FunctionDeclaration' || d.type === 'ClassDeclaration') addId(d.id);
334
- else if (d.type === 'VariableDeclaration') d.declarations.forEach(v => addId(v.id));
335
- } else if (node.type === 'ImportDeclaration') {
336
- // B. Imports (Rewriting references to other bundled modules)
337
- const importPath = node.source.value;
338
- const resolvedPath = resolveImportPath(importPath, sourceFilePath);
339
- // Check if this import is pointing to a bundled file
340
- const isBundled = (bundledPaths && bundledPaths.has(resolvedPath)) || importPath.endsWith('.jux');
341
-
342
- if (isBundled) {
343
- const targetNamespace = resolvedPath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
344
- node.specifiers.forEach(spec => {
345
- if (spec.type === 'ImportSpecifier') {
346
- identifiersToRename.set(spec.local.name, `${spec.imported.name}$${targetNamespace}`);
347
- } else if (spec.type === 'ImportDefaultSpecifier') {
348
- // Best effort for defaults in this flat bundle scheme usually implies named export match or similar convention
349
- // For now, assuming standard View exports which don't usually default export to shared
350
- }
351
- });
352
- }
353
- }
354
- });
355
-
356
- // 3. Reconstruct Code Line-by-Line (Node-by-Node)
357
- const outputLines = [`// From: ${sourceFilePath}`];
358
-
359
- // Sort keys by length desc to avoid partial replacement issues
360
- const sortedKeys = Array.from(identifiersToRename.keys()).sort((a, b) => b.length - a.length);
361
-
362
- ast.body.forEach(node => {
363
- // Skip Imports (handled globally in bundling phase)
364
- if (node.type === 'ImportDeclaration') return;
365
- // Skip "export { a, b }" (re-exports) as 'a' and 'b' are already declared/renamed locally
366
- if (node.type === 'ExportNamedDeclaration' && !node.declaration) return;
367
-
368
- // Extract relevant code slice
369
- let code = '';
370
- if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
371
- if (node.declaration) {
372
- code = juxContent.slice(node.declaration.start, node.declaration.end);
373
- }
374
- } else {
375
- code = juxContent.slice(node.start, node.end);
376
- }
377
-
378
- if (!code) return;
379
-
380
- // Replace Identifiers in this block
381
- // Regex:
382
- // (?<!\.) -> Negative lookbehind to avoid property access (obj.foo)
383
- // \bkey\b -> Word boundary
384
- // (?!\s*:) -> Negative lookahead to avoid object keys ({ foo: 1 })
385
- for (const key of sortedKeys) {
386
- const namespaced = identifiersToRename.get(key);
387
- const regex = new RegExp(`(?<!\\.)\\b${key}\\b(?!\\s*:)`, 'g');
388
- code = code.replace(regex, namespaced);
389
- }
390
-
391
- outputLines.push(code);
392
- });
393
-
394
- return outputLines.join('\n\n');
395
- }
396
-
397
- function transformJuxToViewFunction(juxContent, functionName, pageName, relativePath, sharedModules, bundledPaths) {
398
- let result;
399
- try {
400
- const ast = acorn.parse(juxContent, { ecmaVersion: 'latest', sourceType: 'module' });
401
- const nodesToRemove = [];
402
- const importReplacements = new Map();
403
- ast.body.forEach(node => {
404
- if (node.type === 'ImportDeclaration') {
405
- const importPath = node.source.value;
406
- const resolvedPath = resolveImportPath(importPath, relativePath);
407
-
408
- // ✅ Check if this import is pointing to a bundled file
409
- // Handle BOTH relative and absolute-relative imports (starting with /)
410
- const isBundled = (bundledPaths && bundledPaths.has(resolvedPath)) || importPath.endsWith('.jux');
411
-
412
- if (isBundled) {
413
- node.specifiers.forEach(spec => {
414
- if (spec.type === 'ImportSpecifier') {
415
- // ✅ Apply namespace convention to .js imports too
416
- const namespace = resolvedPath.replace(/\.(jux|js)$/, '').replace(/[\/\\]/g, '$').replace(/[^a-zA-Z0-9$]/g, '_');
417
- importReplacements.set(spec.local.name, `${spec.imported.name}$${namespace}`);
418
- }
419
- });
420
- }
421
-
422
- // 🔥 FIX: Always remove ImportDeclaration from the view function contents.
423
- // Leftover imports inside a function body cause SyntaxError.
424
- nodesToRemove.push({ start: node.start, end: node.end });
425
- }
426
- if (node.type === 'ExportNamedDeclaration' || node.type === 'ExportDefaultDeclaration') {
427
- nodesToRemove.push({ start: node.start, end: node.end });
428
- }
429
- });
430
- nodesToRemove.sort((a, b) => b.start - a.start);
431
- result = juxContent;
432
- nodesToRemove.forEach(({ start, end }) => { result = result.slice(0, start) + result.slice(end); });
433
- importReplacements.forEach((namespacedName, localName) => {
434
- result = result.replace(new RegExp(`\\b${localName}\\b`, 'g'), namespacedName);
435
- });
436
- } catch (err) {
437
- throw new Error(`Invalid JavaScript syntax in ${relativePath}`);
438
- }
439
-
440
- result = result.replace(/\.renderTo\(container\)/g, '.render("#app")').replace(/\.render\(\s*\)/g, '.render("#app")');
441
- const cleanName = functionName.replace(/[-_]/g, ' ').split(' ').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('');
442
-
443
- // ✅ Wrap View implementation in underscore function, then export Wrapped version
444
- return `
445
- // View: ${cleanName}
446
- function _${cleanName}() {
447
- ${result}
448
-
449
- return document.getElementById('app');
450
- }
451
- const ${cleanName} = JuxError.wrap('${cleanName}', _${cleanName});
452
- // Register Source
453
- JuxError.register('${cleanName}', '${relativePath}');
454
- `;
455
- }
456
-
457
- function resolveImportPath(importPath, currentFilePath) {
458
-
459
- console.log(`[Resolve] Importing "${importPath}" from "${currentFilePath}"`);
460
- // ✅ FIX: Handle root-relative imports (start with /)
461
- if (importPath.startsWith('/')) {
462
- const resolved = importPath.substring(1).replace(/\\/g, '/');
463
- // console.log(`[Resolve] Root-relative: "${importPath}" -> "${resolved}"`);
464
- return resolved;
465
- }
466
- const currentDir = path.dirname(currentFilePath);
467
- // ✅ Ensure normalized separators
468
- const resolved = path.join(currentDir, importPath).replace(/\\/g, '/');
469
- // console.log(`[Resolve] Relative: "${importPath}" (from ${currentFilePath}) -> "${resolved}"`);
470
- console.log(`[Resolve] Importing "${importPath}" from "${currentFilePath}" -> Resolved: "${resolved}"`);
471
- return resolved;
472
- }
473
-
474
- function generateRouterBundle(views, routes, sharedModules = new Map(), allImports = [], projectRoot = '') {
475
- const mergedImports = new Map();
476
- const getCanonicalSource = (source, contextFilePath) => {
477
- if (!source.startsWith('.') || source.includes('.jux')) return source;
478
- const dir = path.dirname(contextFilePath);
479
- const joinedPath = path.join(dir, source);
480
- const v2Index = joinedPath.indexOf('lib/componentsv2');
481
- if (v2Index !== -1) {
482
- const subPath = joinedPath.substring(v2Index + 'lib/componentsv2'.length);
483
- if (!subPath || subPath === '/index.js' || subPath === '/index') return 'juxscript';
484
- return 'juxscript' + subPath.replace(/\\/g, '/');
485
- }
486
- if (joinedPath.includes('lib/components')) return 'juxscript/components';
487
-
488
- // Fix: Ensure local relative paths ALWAYS start with ./
489
- let normalized = joinedPath.replace(/\\/g, '/');
490
- // If it's a bare filename like "file.js", prefix it to "./file.js"
491
- // Also check for ":" to avoid messing up URLs like http:// or data:
492
- if (!normalized.startsWith('.') && !normalized.startsWith('/') && !normalized.includes(':')) {
493
- const old = normalized;
494
- normalized = './' + normalized;
495
- console.log(` 🛠️ Canonicalizing local import: "${old}" -> "${normalized}"`);
496
- }
497
- return normalized;
498
- };
499
-
500
- const importList = Array.isArray(allImports) ? allImports : Array.from(allImports);
501
- for (const item of importList) {
502
- try {
503
- const code = typeof item === 'string' ? item : item.code;
504
- const filePath = typeof item === 'string' ? '' : item.filePath;
505
- const ast = acorn.parse(code.trim(), { ecmaVersion: 'latest', sourceType: 'module' });
506
- const node = ast.body[0];
507
- if (node && node.type === 'ImportDeclaration') {
508
- const rawSource = node.source.value;
509
- const source = getCanonicalSource(rawSource, filePath);
510
-
511
- // Debug: Log if an import is being rewritten or handled specially
512
- if (rawSource !== source && !source.startsWith('juxscript')) {
513
- console.log(` 📦 Processing Import: "${rawSource}" -> "${source}" (in ${filePath})`);
514
- }
515
-
516
- if (!mergedImports.has(source)) mergedImports.set(source, { defaults: new Set(), named: new Set(), namespace: null });
517
- const storage = mergedImports.get(source);
518
- node.specifiers.forEach(spec => {
519
- if (spec.type === 'ImportDefaultSpecifier') storage.defaults.add(spec.local.name);
520
- else if (spec.type === 'ImportSpecifier') storage.named.add(spec.imported.name !== spec.local.name ? `${spec.imported.name} as ${spec.local.name}` : spec.imported.name);
521
- else if (spec.type === 'ImportNamespaceSpecifier') storage.namespace = spec.local.name;
522
- });
523
- }
524
- } catch (e) {
525
- console.warn(` ⚠️ Failed to process import in bundle:`, item);
526
- }
527
- }
528
-
529
- const filteredImports = [];
530
- mergedImports.forEach((storage, source) => {
531
- if (storage.namespace) filteredImports.push(`import * as ${storage.namespace} from '${source}';`);
532
- const parts = [];
533
- if (storage.defaults.size > 0) parts.push(Array.from(storage.defaults)[0]);
534
- if (storage.named.size > 0) parts.push(`{ ${Array.from(storage.named).sort().join(', ')} }`);
535
- if (storage.defaults.size === 0 && storage.named.size === 0 && !storage.namespace) filteredImports.push(`import '${source}';`);
536
- else if (parts.length > 0) filteredImports.push(`import ${parts.join(', ')} from '${source}';`);
537
- });
538
-
539
- const juxDebugUtils = `
540
- // ============================================
541
- // JUX DEBUG UTILITIES
542
- // ============================================
543
- const JUX_DEBUG = true;
544
-
545
- const JuxError = {
546
- sourceMap: {},
547
-
548
- register(viewName, sourceFile) {
549
- this.sourceMap[viewName] = sourceFile;
550
- },
551
-
552
- // Global Error Handler Setup
553
- init() {
554
- window.addEventListener('error', (event) => {
555
- JuxError.displayError('Uncaught Exception', event.error);
556
- });
557
- window.addEventListener('unhandledrejection', (event) => {
558
- JuxError.displayError('Unhandled Promise Rejection', event.reason);
559
- });
560
- },
561
-
562
- displayError(title, error) {
563
- console.error(\`🚨 JUX \${title}\`, error);
564
- const app = document.getElementById('app');
565
- if (app) {
566
- // Prevent multiple error overlays stacking
567
- if (document.getElementById('jux-error-overlay')) return;
568
-
569
- app.innerHTML = \`
570
- <div id="jux-error-overlay" style="font-family: monospace; background: #1e1e1e; color: #ff6b6b; min-height: 100vh; padding: 2rem; position:fixed; top:0; left:0; width:100%; z-index:99999;">
571
- <div style="display: flex; align-items: center; gap: 1rem; margin-bottom: 2rem;">
572
- <div style="font-size: 2rem;">🚨</div>
573
- <div>
574
- <h1 style="margin:0; font-size: 1.5rem; color: #ff6b6b;">JUX Runtime Error</h1>
575
- <div style="color: #888; margin-top: 4px;">\${title}</div>
576
- </div>
577
- </div>
578
-
579
- <pre style="background: #2d2d2d; padding: 20px; border-radius: 8px; overflow-x: auto; color: #e0e0e0; border-left: 4px solid #ff6b6b; font-size: 14px; line-height: 1.5;">\${error?.stack || error?.message || String(error)}</pre>
580
-
581
- <button onclick="window.location.reload()" style="margin-top:20px; padding: 10px 20px; background: #333; color: white; border: 1px solid #555; cursor: pointer; border-radius: 4px;">Reload Application</button>
582
- </div>
583
- \`;
584
- }
585
- },
586
-
587
- wrap(viewName, viewFn) {
588
- return function() {
589
- const sourceFile = JuxError.sourceMap[viewName] || 'unknown';
590
- if (JUX_DEBUG) console.log(\`🚀 [JUX] Rendering: \${viewName} (\${sourceFile})\`);
591
-
592
- try {
593
- return viewFn.apply(this, arguments);
594
- } catch (error) {
595
- // Enhance error object with view context if possible
596
- error.message = \`[View: \${viewName}] \${error.message}\`;
597
- JuxError.displayError('View Rendering Error', error);
598
- // We do NOT rethrow here to prevent console noise, as we handled it UI-wise.
599
- // However, stopping execution flow is implicitly handled by not returning valid DOM.
600
- }
601
- };
602
- }
603
- };
604
-
605
- // Initialize Global Handlers
606
- JuxError.init();
607
- `;
608
-
609
- return `// Generated Jux Router Bundle
610
- ${filteredImports.join('\n')}
611
-
612
- ${juxDebugUtils}
613
-
614
- // SHARED MODULES
615
- ${Array.from(sharedModules.values()).filter(c => c.trim()).join('\n\n')}
616
-
617
- // VIEWS (Wrapped in JuxError)
618
- ${views.filter(v => v.trim()).join('\n\n')}
619
-
620
- function JuxNotFound() { jux.heading(1, { text: '404 - Page Not Found' }).render('#app'); return document.getElementById('app'); }
621
- function JuxForbidden() { jux.heading(1, { text: '403 - Forbidden' }).render('#app'); return document.getElementById('app'); }
622
-
623
- const routes = {
624
- ${routes.map(r => ` '${r.path}': ${r.functionName}`).join(',\n')}
625
- };
626
-
627
- const app = document.getElementById('app');
628
- function render() {
629
- let path = location.pathname;
630
- let view = routes[path] || (path.endsWith('/') ? (routes[path + 'index'] || routes[path.slice(0, -1) + '/index']) : routes[path + '/index']);
631
- view = view || JuxNotFound;
632
- app.innerHTML = '';
633
- app.removeAttribute('data-jux-page');
634
- view();
635
- const pageName = Object.entries(routes).find(([p, v]) => v === view)?.[0] || 'not-found';
636
- app.setAttribute('data-jux-page', pageName.replace(/^\\\//, '').replace(/\\\//g, '-'));
637
- }
638
- document.addEventListener('click', e => {
639
- const a = e.target.closest('a');
640
- if (!a || a.dataset.router === 'false' || new URL(a.href).origin !== location.origin) return;
641
- e.preventDefault();
642
- history.pushState({}, '', a.href);
643
- render();
644
- });
645
- window.addEventListener('popstate', render);
646
- render();
647
- `;
648
- }
649
-
650
- export function generateIndexHtml(distDir, bundleResult, options = {}) {
651
- const { mainJsFilename = 'main.js', vendoredPaths = {} } = bundleResult || {};
652
- const { isDev = false, wsPort = 3001 } = options;
653
- const importMapScript = generateVendorImportMap(vendoredPaths);
654
- const hotReloadScript = isDev ? `
655
- <script>
656
- (function() {
657
- let ws = new WebSocket('ws://' + window.location.hostname + ':${wsPort}');
658
- ws.onmessage = (msg) => { if (JSON.parse(msg.data).type === 'reload') window.location.reload(); };
659
- })();
660
- </script>` : '';
661
-
662
- const html = `<!DOCTYPE html>
663
- <html lang="en">
664
- <head>
665
- <meta charset="UTF-8">
666
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
667
- <title>Jux Application</title>
668
- </head>
669
- <body data-theme="">
670
- <div id="app"></div>
671
- ${importMapScript}
672
- <script type="module" src="/${mainJsFilename}"></script>
673
- ${hotReloadScript}
674
- </body>
675
- </html>`;
676
- fs.writeFileSync(path.join(distDir, 'index.html'), html);
677
- }
678
-
679
- async function vendorExternalDependencies(externalDeps, distDir) {
680
- const vendorDir = path.join(distDir, 'vendor');
681
- if (!fs.existsSync(vendorDir)) fs.mkdirSync(vendorDir, { recursive: true });
682
- const vendoredPaths = {};
683
- for (const dep of externalDeps) {
684
- try {
685
- const nodeModulePath = path.join(process.cwd(), 'node_modules', dep);
686
- if (!fs.existsSync(nodeModulePath)) continue;
687
- const pkg = JSON.parse(fs.readFileSync(path.join(nodeModulePath, 'package.json'), 'utf-8'));
688
- const entryPoints = [pkg.module, pkg.browser, pkg.main, 'index.js'].filter(Boolean);
689
- let entryFile = entryPoints.map(e => path.join(nodeModulePath, e)).find(p => fs.existsSync(p));
690
- if (!entryFile) continue;
691
- await esbuild.build({ entryPoints: [entryFile], bundle: true, format: 'esm', outfile: path.join(vendorDir, `${dep}.js`), platform: 'browser', target: 'es2020', logLevel: 'silent' });
692
- vendoredPaths[dep] = `/vendor/${dep}.js`;
693
- } catch (err) { }
694
- }
695
- return vendoredPaths;
696
- }
697
-
698
- function generateVendorImportMap(vendoredPaths) {
699
- const imports = {
700
- "juxscript": "/lib/componentsv2/index.js",
701
- "juxscript/": "/lib/componentsv2/",
702
- ...vendoredPaths
703
- };
704
- Object.keys(imports).forEach(key => { if (imports[key].startsWith('./')) imports[key] = imports[key].substring(1); });
705
- return `<script type="importmap">\n${JSON.stringify({ imports }, null, 2)}\n</script>`;
706
- }