juxscript 1.1.140 → 1.1.141

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,713 +0,0 @@
1
- import * as esbuild from 'esbuild';
2
- import * as acorn from 'acorn';
3
- import { walk } from 'astray';
4
- import fs from 'fs';
5
- import path from 'path';
6
- import { fileURLToPath } from 'url';
7
-
8
- const __filename = fileURLToPath(import.meta.url);
9
- const __dirname = path.dirname(__filename);
10
-
11
- export class JuxCompiler {
12
- constructor(config = {}) {
13
- this.config = config;
14
- this.srcDir = config.srcDir || './jux';
15
- this.distDir = config.distDir || './.jux-dist';
16
- this.publicDir = config.publicDir || './public'; // ✅ Configurable public path
17
- this.defaults = config.defaults || {};
18
- this.paths = config.paths || {};
19
- this._juxscriptExports = null;
20
- this._juxscriptPath = null;
21
- this._renderFunctionCounter = 0; // ✅ Add counter for unique IDs
22
- }
23
-
24
- /**
25
- * Locate juxscript package - simplified resolution
26
- */
27
- findJuxscriptPath() {
28
- if (this._juxscriptPath) return this._juxscriptPath;
29
-
30
- const projectRoot = process.cwd();
31
-
32
- // Priority 1: User's node_modules (when used as dependency)
33
- const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
34
- if (fs.existsSync(userPath)) {
35
- this._juxscriptPath = userPath;
36
- return userPath;
37
- }
38
-
39
- // Priority 2: Package root (when developing juxscript itself)
40
- const packageRoot = path.resolve(__dirname, '..');
41
- const devPath = path.resolve(packageRoot, 'index.js');
42
- if (fs.existsSync(devPath)) {
43
- this._juxscriptPath = devPath;
44
- return devPath;
45
- }
46
-
47
- // Priority 3: Sibling in monorepo
48
- const monoPath = path.resolve(projectRoot, '../jux/index.js');
49
- if (fs.existsSync(monoPath)) {
50
- this._juxscriptPath = monoPath;
51
- return monoPath;
52
- }
53
-
54
- return null;
55
- }
56
-
57
- /**
58
- * ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
59
- */
60
- scanFiles() {
61
- const views = [], dataModules = [], sharedModules = [];
62
- const processedNames = new Set(); // Track processed file base names to avoid duplicates
63
-
64
- /**
65
- * Recursive directory scanner
66
- */
67
- const scanDirectory = (currentDir) => {
68
- const entries = fs.readdirSync(currentDir, { withFileTypes: true });
69
-
70
- for (const entry of entries) {
71
- const fullPath = path.join(currentDir, entry.name);
72
-
73
- if (entry.isDirectory()) {
74
- // ✅ Recurse into subdirectories
75
- scanDirectory(fullPath);
76
- } else if (entry.isFile()) {
77
- const file = entry.name;
78
-
79
- // ✅ Only process .jux and .js files (skip assets)
80
- if ((file.endsWith('.jux') || file.endsWith('.js')) && !this.isAssetFile(file)) {
81
- const content = fs.readFileSync(fullPath, 'utf8');
82
- const relativePath = path.relative(this.srcDir, fullPath);
83
- const name = relativePath.replace(/\.[^/.]+$/, ''); // Remove extension
84
-
85
- // ✅ Generate unique key that includes full path to avoid collisions
86
- // This handles cases like:
87
- // - apps/audits/audits.jux → apps_audits_audits
88
- // - apps/audits.jux → apps_audits
89
- const uniqueKey = name.replace(/[\/\\]/g, '_').replace(/[^a-zA-Z0-9_]/g, '_');
90
-
91
- // ✅ Skip if we've already processed this exact path
92
- if (processedNames.has(uniqueKey)) {
93
- console.warn(`⚠️ Skipping duplicate: ${relativePath} (already processed as ${uniqueKey})`);
94
- continue;
95
- }
96
- processedNames.add(uniqueKey);
97
-
98
- if (file.includes('data')) {
99
- dataModules.push({ name, file: relativePath, content });
100
- } else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
101
- sharedModules.push({ name, file: relativePath, content });
102
- } else {
103
- views.push({ name, file: relativePath, content });
104
- }
105
- }
106
- }
107
- }
108
- };
109
-
110
- // ✅ Start recursive scan from srcDir
111
- scanDirectory(this.srcDir);
112
-
113
- return { views, dataModules, sharedModules };
114
- }
115
-
116
- isAssetFile(filename) {
117
- const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
118
- return assetExtensions.some(ext => filename.endsWith(ext));
119
- }
120
-
121
- removeImports(code) {
122
- return code
123
- .replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
124
- .replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
125
- .replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
126
- .replace(/^\s*import\s*;?\s*$/gm, '');
127
- }
128
-
129
- sanitizeName(name) {
130
- return name.replace(/[^a-zA-Z0-9]/g, '_');
131
- }
132
-
133
- async loadJuxscriptExports() {
134
- if (this._juxscriptExports) return this._juxscriptExports;
135
-
136
- try {
137
- const juxscriptPath = this.findJuxscriptPath();
138
- if (juxscriptPath) {
139
- const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
140
- const exports = new Set();
141
-
142
- for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
143
- match[1].split(',').forEach(exp => {
144
- const name = exp.trim().split(/\s+as\s+/)[0].trim();
145
- if (name) exports.add(name);
146
- });
147
- }
148
-
149
- this._juxscriptExports = [...exports];
150
- if (this._juxscriptExports.length > 0) {
151
- console.log(`📦 juxscript exports: ${this._juxscriptExports.join(', ')}`);
152
- }
153
- }
154
- } catch (err) {
155
- this._juxscriptExports = [];
156
- }
157
-
158
- return this._juxscriptExports;
159
- }
160
-
161
- validateViewCode(viewName, code) {
162
- const issues = [];
163
-
164
- let ast;
165
- try {
166
- ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
167
- } catch (parseError) {
168
- issues.push({
169
- type: 'error',
170
- view: viewName,
171
- line: parseError.loc?.line || 0,
172
- message: `Syntax error: ${parseError.message}`,
173
- code: ''
174
- });
175
- return issues;
176
- }
177
-
178
- const allImports = new Set();
179
-
180
- walk(ast, {
181
- ImportDeclaration(node) {
182
- node.specifiers.forEach(spec => {
183
- allImports.add(spec.local.name);
184
- });
185
- }
186
- });
187
-
188
- // Default known facades/components if load fails
189
- const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
190
-
191
- walk(ast, {
192
- Identifier(node, parent) {
193
- if (parent?.type === 'CallExpression' && parent.callee === node) {
194
- const name = node.name;
195
- if (!allImports.has(name) && knownComponents.includes(name)) {
196
- issues.push({
197
- type: 'warning',
198
- view: viewName,
199
- line: node.loc?.start?.line || 0,
200
- message: `"${name}" is used but not imported from juxscript`,
201
- code: ''
202
- });
203
- }
204
- }
205
- }
206
- });
207
-
208
- return issues;
209
- }
210
-
211
- /**
212
- * Generate entry point without layout/theme logic
213
- */
214
- generateEntryPoint(views, dataModules, sharedModules) {
215
- let entry = `// Auto-generated JUX entry point\n\n`;
216
- const allIssues = [];
217
- const sourceSnapshot = {};
218
-
219
- const juxImports = new Set();
220
- [...views, ...dataModules, ...sharedModules].forEach(m => {
221
- for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
222
- match[1].split(',').map(s => s.trim()).forEach(imp => {
223
- if (imp) juxImports.add(imp);
224
- });
225
- }
226
- });
227
-
228
- if (juxImports.size > 0) {
229
- entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
230
- }
231
-
232
- dataModules.forEach(m => {
233
- entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
234
- });
235
- sharedModules.forEach(m => {
236
- entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
237
- });
238
-
239
- entry += `\n// Expose to window\n`;
240
- dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
241
- sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
242
-
243
- if (juxImports.size > 0) {
244
- entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
245
- }
246
-
247
- entry += `\n// --- VIEW FUNCTIONS ---\n`;
248
-
249
- // ✅ Map to store route → function name mappings
250
- const routeToFunctionMap = new Map();
251
-
252
- views.forEach(v => {
253
- // ✅ Generate TRULY UNIQUE serial ID
254
- const serialId = this._renderFunctionCounter++;
255
- const functionName = `renderJux${serialId}`;
256
-
257
- sourceSnapshot[v.file] = {
258
- name: v.name,
259
- file: v.file,
260
- content: v.content,
261
- lines: v.content.split('\n'),
262
- functionName // ✅ Store the unique function name
263
- };
264
-
265
- let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
266
- const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
267
-
268
- // ✅ Use truly unique serial function name
269
- entry += `\n${asyncPrefix}function ${functionName}() {\n${viewCode}\n}\n`;
270
-
271
- // ✅ Generate URL-safe route path from original name
272
- const routePath = v.name
273
- .toLowerCase()
274
- .replace(/\\/g, '/')
275
- .replace(/\.jux$/i, '')
276
- .replace(/\./g, '-')
277
- .replace(/\s+/g, '-')
278
- .replace(/[^a-z0-9\/_-]/g, '')
279
- .replace(/-+/g, '-')
280
- .replace(/^-|-$/g, '');
281
-
282
- // ✅ Handle index route
283
- if (routePath === 'index' || routePath === '') {
284
- routeToFunctionMap.set('/', functionName);
285
- }
286
-
287
- // ✅ Map route to function
288
- routeToFunctionMap.set(`/${routePath}`, functionName);
289
- });
290
-
291
- dataModules.forEach(m => {
292
- sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
293
- });
294
- sharedModules.forEach(m => {
295
- sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
296
- });
297
-
298
- this._sourceSnapshot = sourceSnapshot;
299
- this._validationIssues = allIssues;
300
-
301
- // ✅ Pass the route map to router generator
302
- entry += this._generateRouter(routeToFunctionMap);
303
- return entry;
304
- }
305
-
306
- reportValidationIssues() {
307
- const issues = this._validationIssues || [];
308
- const errors = issues.filter(i => i.type === 'error');
309
- const warnings = issues.filter(i => i.type === 'warning');
310
-
311
- if (issues.length > 0) {
312
- console.log('\n⚠️ Validation Issues:\n');
313
- issues.forEach(issue => {
314
- const icon = issue.type === 'error' ? '❌' : '⚠️';
315
- console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
316
- });
317
- console.log('');
318
- }
319
-
320
- return { isValid: errors.length === 0, errors, warnings };
321
- }
322
-
323
- _generateRouter(routeToFunctionMap) {
324
- // ✅ Build route map from Map object
325
- let routeMap = '';
326
- routeToFunctionMap.forEach((functionName, route) => {
327
- routeMap += ` '${route}': ${functionName},\n`;
328
- });
329
-
330
- return `
331
- // --- JUX SOURCE LOADER ---
332
- var __juxSources = null;
333
- async function __juxLoadSources() {
334
- if (__juxSources) return __juxSources;
335
- try {
336
- var res = await fetch('/__jux_sources.json');
337
- __juxSources = await res.json();
338
- } catch (e) {
339
- __juxSources = {};
340
- }
341
- return __juxSources;
342
- }
343
-
344
- function __juxFindSource(stack) {
345
- var match = stack.match(/renderJux(\\d+)/);
346
- if (match) {
347
- var functionName = 'renderJux' + match[1];
348
- for (var file in __juxSources || {}) {
349
- if (__juxSources[file].functionName === functionName) {
350
- return { file: file, source: __juxSources[file], functionName: functionName };
351
- }
352
- }
353
- }
354
- return null;
355
- }
356
-
357
- // --- JUX RUNTIME ERROR OVERLAY ---
358
- var __juxErrorOverlay = {
359
- styles: \`
360
- #__jux-error-overlay {
361
- position: fixed; inset: 0; z-index: 99999;
362
- background: rgba(0, 0, 0, 0.4);
363
- display: flex; align-items: center; justify-content: center;
364
- font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
365
- opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
366
- }
367
- #__jux-error-overlay.visible { opacity: 1; }
368
- #__jux-error-overlay * { box-sizing: border-box; }
369
- .__jux-modal {
370
- background: #f8f9fa; border-radius: 4px;
371
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
372
- max-width: 80vw; width: 90%; max-height: 90vh;
373
- overflow: hidden; display: flex; flex-direction: column;
374
- transform: translateY(10px); transition: transform 0.2s ease-out;
375
- }
376
- #__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
377
- .__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
378
- .__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
379
- .__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
380
- .__jux-header .file-info { color: #6b7280; font-size: 13px; }
381
- .__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
382
- .__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
383
- .__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
384
- .__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
385
- .__jux-code-line.error { background: #fef2f2; }
386
- .__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
387
- .__jux-code-line.context { background: #fefce8; }
388
- .__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
389
- .__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
390
- .__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
391
- .__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
392
- .__jux-tip { color: #6b7280; font-size: 12px; }
393
- .__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
394
- .__jux-dismiss:hover { background: #e5e7eb; }
395
- .__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
396
- .__jux-no-source pre { background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px; font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left; border: 1px solid #e5e7eb; }
397
- \`,
398
-
399
- show: async function(error, title) {
400
- title = title || 'Runtime Error';
401
- var existing = document.getElementById('__jux-error-overlay');
402
- if (existing) existing.remove();
403
- await __juxLoadSources();
404
-
405
- var overlay = document.createElement('div');
406
- overlay.id = '__jux-error-overlay';
407
- var stack = error && error.stack ? error.stack : '';
408
- var msg = error && error.message ? error.message : String(error);
409
- var found = __juxFindSource(stack);
410
- var sourceHtml = '', fileInfo = '';
411
-
412
- if (found && found.source && found.source.lines) {
413
- var lines = found.source.lines;
414
- fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
415
- var errorLineIndex = -1;
416
- var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
417
- if (errorMethod) {
418
- for (var i = 0; i < lines.length; i++) {
419
- if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
420
- }
421
- }
422
- if (errorLineIndex === -1) {
423
- for (var i = 0; i < lines.length; i++) {
424
- if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
425
- }
426
- }
427
- var contextStart = Math.max(0, errorLineIndex - 3);
428
- var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
429
- if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
430
-
431
- for (var i = contextStart; i <= contextEnd; i++) {
432
- var isError = (i === errorLineIndex);
433
- var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
434
- var lineClass = isError ? ' error' : (isContext ? ' context' : '');
435
- var lineCode = lines[i].replace(/</g, '&lt;').replace(/>/g, '&gt;') || ' ';
436
- sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
437
- }
438
- } else {
439
- sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
440
- }
441
-
442
- overlay.innerHTML = '<style>' + this.styles + '</style><div class="__jux-modal"><div class="__jux-header"><h3>' + title + '</h3><h1>' + msg + '</h1>' + fileInfo + '</div><div class="__jux-source"><div class="__jux-code">' + sourceHtml + '</div></div><div class="__jux-footer"><span class="__jux-tip">💡 Fix the error and save to reload</span><button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button></div></div>';
443
- document.body.appendChild(overlay);
444
- requestAnimationFrame(function() { overlay.classList.add('visible'); });
445
- console.error(title + ':', error);
446
- }
447
- };
448
-
449
- window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
450
- window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
451
-
452
- // --- JUX ROUTER ---
453
- const routes = {\n${routeMap}};
454
-
455
- // Simple router
456
- function route(path) {
457
- const renderFn = routes[path] || routes['/'];
458
- if (renderFn) {
459
- document.getElementById('app').innerHTML = '';
460
- renderFn();
461
- } else {
462
- document.getElementById('app').innerHTML = '<h1>404 - Page Not Found</h1>';
463
- }
464
- }
465
-
466
- // Initial route
467
- route(window.location.pathname);
468
-
469
- // Handle navigation
470
- window.addEventListener('popstate', () => route(window.location.pathname));
471
-
472
- // Intercept link clicks
473
- document.addEventListener('click', (e) => {
474
- if (e.target.matches('a[href]')) {
475
- const href = e.target.getAttribute('href');
476
- if (href.startsWith('/') && !href.startsWith('//')) {
477
- e.preventDefault();
478
- window.history.pushState({}, '', href);
479
- route(href);
480
- }
481
- }
482
- });
483
- `;
484
- }
485
-
486
- async build() {
487
- console.log('🚀 JUX Build\n');
488
-
489
- const juxscriptPath = this.findJuxscriptPath();
490
- if (!juxscriptPath) {
491
- console.error('❌ Could not locate juxscript package');
492
- return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
493
- }
494
- console.log(`📦 Using: ${juxscriptPath}`);
495
-
496
- await this.loadJuxscriptExports();
497
-
498
- if (fs.existsSync(this.distDir)) {
499
- fs.rmSync(this.distDir, { recursive: true, force: true });
500
- }
501
- fs.mkdirSync(this.distDir, { recursive: true });
502
-
503
- // ✅ Copy public folder if exists
504
- this.copyPublicFolder();
505
-
506
- const { views, dataModules, sharedModules } = this.scanFiles();
507
- console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
508
-
509
- // Copy data/shared modules to dist
510
- const juxDistDir = path.join(this.distDir, 'jux');
511
- fs.mkdirSync(juxDistDir, { recursive: true });
512
-
513
- // ✅ Create subdirectories and copy files
514
- [...dataModules, ...sharedModules].forEach(m => {
515
- const destPath = path.join(juxDistDir, m.file);
516
- const destDir = path.dirname(destPath);
517
-
518
- if (!fs.existsSync(destDir)) {
519
- fs.mkdirSync(destDir, { recursive: true });
520
- }
521
-
522
- fs.writeFileSync(destPath, m.content);
523
- });
524
-
525
- const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
526
-
527
- const entryPath = path.join(this.distDir, 'entry.js');
528
- fs.writeFileSync(entryPath, entryContent);
529
-
530
- const snapshotPath = path.join(this.distDir, '__jux_sources.json');
531
- fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
532
- console.log(`📸 Source snapshot written`);
533
-
534
- const validation = this.reportValidationIssues();
535
- if (!validation.isValid) {
536
- console.log('🛑 BUILD FAILED\n');
537
- return { success: false, errors: validation.errors, warnings: validation.warnings };
538
- }
539
-
540
- try {
541
- await esbuild.build({
542
- entryPoints: [entryPath],
543
- bundle: true,
544
- outfile: path.join(this.distDir, 'bundle.js'),
545
- format: 'esm',
546
- platform: 'browser',
547
- target: 'esnext',
548
- sourcemap: true,
549
-
550
- loader: {
551
- '.jux': 'js',
552
- '.css': 'empty'
553
- },
554
-
555
- plugins: [{
556
- name: 'juxscript-resolver',
557
- setup: (build) => {
558
- // Only resolve juxscript - everything else gets bundled normally
559
- build.onResolve({ filter: /^juxscript$/ }, () => ({
560
- path: juxscriptPath
561
- }));
562
- }
563
- }],
564
-
565
- // ✅ FIX: Only mark true Node.js built-ins as external
566
- // Remove axios-related modules from external list
567
- external: [],
568
-
569
- // ✅ Define process.env and global for browser
570
- define: {
571
- 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
572
- 'global': 'globalThis'
573
- },
574
-
575
- // ✅ Provide empty shims for Node.js built-ins
576
- inject: [],
577
-
578
- // ✅ Minify only in production
579
- minify: process.env.NODE_ENV === 'production',
580
- treeShaking: true,
581
- });
582
-
583
- console.log('✅ esbuild complete');
584
-
585
- // ✅ Show bundle size
586
- const bundlePath = path.join(this.distDir, 'bundle.js');
587
- const bundleStats = fs.statSync(bundlePath);
588
- const bundleSizeKB = (bundleStats.size / 1024).toFixed(2);
589
- console.log(`📦 Bundle size: ${bundleSizeKB} KB`);
590
-
591
- } catch (err) {
592
- console.error('❌ esbuild failed:', err);
593
- return { success: false, errors: [{ message: err.message }], warnings: [] };
594
- }
595
-
596
- const html = `<!DOCTYPE html>
597
- <html lang="en">
598
- <head>
599
- <meta charset="UTF-8">
600
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
601
- <title>JUX Application</title>
602
- <script type="module" src="/bundle.js"></script>
603
- <script src="/entry.js"></script>
604
- </head>
605
- <body>
606
- <div id="app"></div>
607
- </body>
608
- </html>`;
609
-
610
- fs.writeFileSync(
611
- path.join(this.config.distDir, 'index.html'),
612
- html,
613
- 'utf8'
614
- );
615
-
616
- console.log(' ✅ Generated index.html');
617
- return { success: true, errors: [], warnings: validation.warnings };
618
- }
619
-
620
- /**
621
- * Copy public folder contents to dist
622
- */
623
- copyPublicFolder() {
624
- // ✅ Use configured public path or resolve from paths object
625
- const publicSrc = this.paths.public
626
- ? this.paths.public
627
- : path.resolve(process.cwd(), this.publicDir);
628
-
629
- if (!fs.existsSync(publicSrc)) {
630
- return; // No public folder, skip
631
- }
632
-
633
- console.log('📦 Copying public assets...');
634
-
635
- try {
636
- this._copyDirRecursive(publicSrc, this.distDir, 0);
637
- console.log('✅ Public assets copied');
638
- } catch (err) {
639
- console.warn('⚠️ Error copying public folder:', err.message);
640
- }
641
- }
642
-
643
- /**
644
- * Recursively copy directory contents
645
- */
646
- _copyDirRecursive(src, dest, depth = 0) {
647
- const entries = fs.readdirSync(src, { withFileTypes: true });
648
-
649
- entries.forEach(entry => {
650
- // Skip hidden files and directories
651
- if (entry.name.startsWith('.')) return;
652
-
653
- const srcPath = path.join(src, entry.name);
654
- const destPath = path.join(dest, entry.name);
655
-
656
- if (entry.isDirectory()) {
657
- // Create directory and recurse
658
- if (!fs.existsSync(destPath)) {
659
- fs.mkdirSync(destPath, { recursive: true });
660
- }
661
- this._copyDirRecursive(srcPath, destPath, depth + 1);
662
- } else {
663
- // Copy file
664
- fs.copyFileSync(srcPath, destPath);
665
-
666
- // Log files at root level only
667
- if (depth === 0) {
668
- const ext = path.extname(entry.name);
669
- const icon = this._getFileIcon(ext);
670
- console.log(` ${icon} ${entry.name}`);
671
- }
672
- }
673
- });
674
- }
675
-
676
- /**
677
- * Get icon for file type
678
- */
679
- _getFileIcon(ext) {
680
- const icons = {
681
- '.html': '📄',
682
- '.css': '🎨',
683
- '.js': '📜',
684
- '.json': '📋',
685
- '.png': '🖼️',
686
- '.jpg': '🖼️',
687
- '.jpeg': '🖼️',
688
- '.gif': '🖼️',
689
- '.svg': '🎨',
690
- '.ico': '🔖',
691
- '.woff': '🔤',
692
- '.woff2': '🔤',
693
- '.ttf': '🔤',
694
- '.eot': '🔤'
695
- };
696
- return icons[ext.toLowerCase()] || '📦';
697
- }
698
-
699
- /**
700
- * ✅ Generate valid JavaScript identifier from file path
701
- * Example: abc/aaa.jux -> abc_aaa
702
- * Example: menus/main.jux -> menus_main
703
- * Example: pages/blog/post.jux -> pages_blog_post
704
- */
705
- _generateNameFromPath(filepath) {
706
- return filepath
707
- .replace(/\.jux$/, '') // Remove .jux extension
708
- .replace(/[\/\\]/g, '_') // Replace / and \ with _
709
- .replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
710
- .replace(/_+/g, '_') // Collapse multiple consecutive underscores
711
- .replace(/^_|_$/g, ''); // Remove leading/trailing underscores
712
- }
713
- }