juxscript 1.1.128 → 1.1.129
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/dom-structure-map.json +1 -1
- package/machinery/compiler3.js +150 -488
- package/package.json +1 -1
package/dom-structure-map.json
CHANGED
package/machinery/compiler3.js
CHANGED
|
@@ -1,66 +1,25 @@
|
|
|
1
|
-
import * as esbuild from 'esbuild';
|
|
2
|
-
import * as acorn from 'acorn';
|
|
3
|
-
import { walk } from 'astray';
|
|
4
1
|
import fs from 'fs';
|
|
5
2
|
import path from 'path';
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
-
const __dirname = path.dirname(__filename);
|
|
3
|
+
import esbuild from 'esbuild';
|
|
10
4
|
|
|
11
5
|
export class JuxCompiler {
|
|
12
|
-
constructor(config
|
|
6
|
+
constructor(config) {
|
|
13
7
|
this.config = config;
|
|
14
|
-
this.srcDir = config.srcDir
|
|
15
|
-
this.distDir = config.distDir
|
|
16
|
-
this.publicDir = config.publicDir
|
|
17
|
-
this.
|
|
18
|
-
this.paths = config.paths || {};
|
|
19
|
-
this._juxscriptExports = null;
|
|
20
|
-
this._juxscriptPath = null;
|
|
8
|
+
this.srcDir = config.srcDir;
|
|
9
|
+
this.distDir = config.distDir;
|
|
10
|
+
this.publicDir = config.publicDir;
|
|
11
|
+
this.paths = config.paths;
|
|
21
12
|
}
|
|
22
13
|
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
*/
|
|
26
|
-
findJuxscriptPath() {
|
|
27
|
-
if (this._juxscriptPath) return this._juxscriptPath;
|
|
28
|
-
|
|
29
|
-
const projectRoot = process.cwd();
|
|
30
|
-
|
|
31
|
-
// Priority 1: User's node_modules (when used as dependency)
|
|
32
|
-
const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
|
|
33
|
-
if (fs.existsSync(userPath)) {
|
|
34
|
-
this._juxscriptPath = userPath;
|
|
35
|
-
return userPath;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Priority 2: Package root (when developing juxscript itself)
|
|
39
|
-
const packageRoot = path.resolve(__dirname, '..');
|
|
40
|
-
const devPath = path.resolve(packageRoot, 'index.js');
|
|
41
|
-
if (fs.existsSync(devPath)) {
|
|
42
|
-
this._juxscriptPath = devPath;
|
|
43
|
-
return devPath;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
// Priority 3: Sibling in monorepo
|
|
47
|
-
const monoPath = path.resolve(projectRoot, '../jux/index.js');
|
|
48
|
-
if (fs.existsSync(monoPath)) {
|
|
49
|
-
this._juxscriptPath = monoPath;
|
|
50
|
-
return monoPath;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
14
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
15
|
+
* SCAN FILES (Recursive)
|
|
16
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
55
17
|
|
|
56
|
-
/**
|
|
57
|
-
* ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
|
|
58
|
-
*/
|
|
59
18
|
scanFiles() {
|
|
60
19
|
const views = [], dataModules = [], sharedModules = [];
|
|
61
20
|
|
|
62
21
|
/**
|
|
63
|
-
* Recursive directory scanner
|
|
22
|
+
* ✅ Recursive directory scanner
|
|
64
23
|
*/
|
|
65
24
|
const scanDirectory = (currentDir) => {
|
|
66
25
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -99,146 +58,37 @@ export class JuxCompiler {
|
|
|
99
58
|
}
|
|
100
59
|
|
|
101
60
|
isAssetFile(filename) {
|
|
102
|
-
const assetExtensions = ['.css', '.
|
|
61
|
+
const assetExtensions = ['.css', '.scss', '.sass', '.less', '.jpg', '.jpeg', '.png', '.gif', '.svg', '.ico', '.woff', '.woff2', '.ttf', '.eot', '.otf'];
|
|
103
62
|
return assetExtensions.some(ext => filename.endsWith(ext));
|
|
104
63
|
}
|
|
105
64
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
.replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
|
|
110
|
-
.replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
111
|
-
.replace(/^\s*import\s*;?\s*$/gm, '');
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
sanitizeName(name) {
|
|
115
|
-
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
async loadJuxscriptExports() {
|
|
119
|
-
if (this._juxscriptExports) return this._juxscriptExports;
|
|
120
|
-
|
|
121
|
-
try {
|
|
122
|
-
const juxscriptPath = this.findJuxscriptPath();
|
|
123
|
-
if (juxscriptPath) {
|
|
124
|
-
const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
|
|
125
|
-
const exports = new Set();
|
|
126
|
-
|
|
127
|
-
for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
|
|
128
|
-
match[1].split(',').forEach(exp => {
|
|
129
|
-
const name = exp.trim().split(/\s+as\s+/)[0].trim();
|
|
130
|
-
if (name) exports.add(name);
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
this._juxscriptExports = [...exports];
|
|
135
|
-
if (this._juxscriptExports.length > 0) {
|
|
136
|
-
console.log(`📦 juxscript exports: ${this._juxscriptExports.join(', ')}`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
} catch (err) {
|
|
140
|
-
this._juxscriptExports = [];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return this._juxscriptExports;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
validateViewCode(viewName, code) {
|
|
147
|
-
const issues = [];
|
|
148
|
-
|
|
149
|
-
let ast;
|
|
150
|
-
try {
|
|
151
|
-
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
152
|
-
} catch (parseError) {
|
|
153
|
-
issues.push({
|
|
154
|
-
type: 'error',
|
|
155
|
-
view: viewName,
|
|
156
|
-
line: parseError.loc?.line || 0,
|
|
157
|
-
message: `Syntax error: ${parseError.message}`,
|
|
158
|
-
code: ''
|
|
159
|
-
});
|
|
160
|
-
return issues;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const allImports = new Set();
|
|
164
|
-
|
|
165
|
-
walk(ast, {
|
|
166
|
-
ImportDeclaration(node) {
|
|
167
|
-
node.specifiers.forEach(spec => {
|
|
168
|
-
allImports.add(spec.local.name);
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Default known facades/components if load fails
|
|
174
|
-
const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
|
|
175
|
-
|
|
176
|
-
walk(ast, {
|
|
177
|
-
Identifier(node, parent) {
|
|
178
|
-
if (parent?.type === 'CallExpression' && parent.callee === node) {
|
|
179
|
-
const name = node.name;
|
|
180
|
-
if (!allImports.has(name) && knownComponents.includes(name)) {
|
|
181
|
-
issues.push({
|
|
182
|
-
type: 'warning',
|
|
183
|
-
view: viewName,
|
|
184
|
-
line: node.loc?.start?.line || 0,
|
|
185
|
-
message: `"${name}" is used but not imported from juxscript`,
|
|
186
|
-
code: ''
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
return issues;
|
|
194
|
-
}
|
|
65
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
66
|
+
* GENERATE ENTRY POINT
|
|
67
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
195
68
|
|
|
196
|
-
/**
|
|
197
|
-
* Generate entry point without layout/theme logic
|
|
198
|
-
*/
|
|
199
69
|
generateEntryPoint(views, dataModules, sharedModules) {
|
|
200
|
-
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
201
|
-
const allIssues = [];
|
|
202
70
|
const sourceSnapshot = {};
|
|
71
|
+
let entry = `/* Auto-generated entry point */\n`;
|
|
72
|
+
entry += `import { jux, state, registry } from 'juxscript';\n\n`;
|
|
203
73
|
|
|
204
|
-
|
|
205
|
-
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
206
|
-
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
207
|
-
match[1].split(',').map(s => s.trim()).forEach(imp => {
|
|
208
|
-
if (imp) juxImports.add(imp);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
if (juxImports.size > 0) {
|
|
214
|
-
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
dataModules.forEach(m => {
|
|
218
|
-
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
219
|
-
});
|
|
74
|
+
// Import shared modules
|
|
220
75
|
sharedModules.forEach(m => {
|
|
221
|
-
entry +=
|
|
76
|
+
entry += `// Shared: ${m.file}\n${m.content}\n\n`;
|
|
222
77
|
});
|
|
223
78
|
|
|
224
|
-
|
|
225
|
-
dataModules.forEach(
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (juxImports.size > 0) {
|
|
229
|
-
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
79
|
+
// Import data modules
|
|
80
|
+
dataModules.forEach(d => {
|
|
81
|
+
entry += `// Data: ${d.file}\n${d.content}\n\n`;
|
|
82
|
+
});
|
|
233
83
|
|
|
84
|
+
// Generate render functions for views
|
|
234
85
|
views.forEach(v => {
|
|
235
86
|
// ✅ Sanitize the name for use in function names
|
|
236
|
-
// Replace slashes, dots, and other invalid characters with underscores
|
|
237
87
|
const sanitizedName = v.name
|
|
238
|
-
.replace(/[\/\\.\\-\s]/g, '_')
|
|
239
|
-
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
240
|
-
.replace(/_+/g, '_')
|
|
241
|
-
.replace(/^_|_$/g, '');
|
|
88
|
+
.replace(/[\/\\.\\-\s]/g, '_')
|
|
89
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
90
|
+
.replace(/_+/g, '_')
|
|
91
|
+
.replace(/^_|_$/g, '');
|
|
242
92
|
|
|
243
93
|
const capitalized = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
|
|
244
94
|
|
|
@@ -252,44 +102,12 @@ export class JuxCompiler {
|
|
|
252
102
|
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
253
103
|
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
254
104
|
|
|
255
|
-
// ✅ Use sanitized name in function declaration
|
|
256
105
|
entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
|
|
257
106
|
});
|
|
258
107
|
|
|
259
|
-
|
|
260
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
261
|
-
});
|
|
262
|
-
sharedModules.forEach(m => {
|
|
263
|
-
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
264
|
-
});
|
|
265
|
-
|
|
266
|
-
this._sourceSnapshot = sourceSnapshot;
|
|
267
|
-
this._validationIssues = allIssues;
|
|
268
|
-
entry += this._generateRouter(views);
|
|
269
|
-
return entry;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
reportValidationIssues() {
|
|
273
|
-
const issues = this._validationIssues || [];
|
|
274
|
-
const errors = issues.filter(i => i.type === 'error');
|
|
275
|
-
const warnings = issues.filter(i => i.type === 'warning');
|
|
276
|
-
|
|
277
|
-
if (issues.length > 0) {
|
|
278
|
-
console.log('\n⚠️ Validation Issues:\n');
|
|
279
|
-
issues.forEach(issue => {
|
|
280
|
-
const icon = issue.type === 'error' ? '❌' : '⚠️';
|
|
281
|
-
console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
|
|
282
|
-
});
|
|
283
|
-
console.log('');
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
return { isValid: errors.length === 0, errors, warnings };
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
_generateRouter(views) {
|
|
108
|
+
// ✅ Generate route map with sanitized names
|
|
290
109
|
let routeMap = '';
|
|
291
110
|
views.forEach(v => {
|
|
292
|
-
// ✅ Sanitize function name (same as above)
|
|
293
111
|
const sanitizedName = v.name
|
|
294
112
|
.replace(/[\/\\.\\-\s]/g, '_')
|
|
295
113
|
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
@@ -298,224 +116,162 @@ export class JuxCompiler {
|
|
|
298
116
|
|
|
299
117
|
const cap = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
|
|
300
118
|
|
|
301
|
-
// ✅ Generate URL-safe route path
|
|
302
|
-
// Convert: 'menus/main' → '/menus/main'
|
|
303
|
-
// Convert: 'about-us' → '/about-us'
|
|
304
|
-
// Convert: 'blog.post' → '/blog-post' (dots become dashes for URLs)
|
|
119
|
+
// ✅ Generate URL-safe route path
|
|
305
120
|
const routePath = v.name
|
|
306
121
|
.toLowerCase()
|
|
307
|
-
.replace(/\\/g, '/')
|
|
308
|
-
.replace(/\.jux$/i, '')
|
|
309
|
-
.replace(/\./g, '-')
|
|
310
|
-
.replace(/\s+/g, '-')
|
|
311
|
-
.replace(/[^a-z0-9\/_-]/g, '')
|
|
312
|
-
.replace(/-+/g, '-')
|
|
313
|
-
.replace(/^-|-$/g, '');
|
|
314
|
-
|
|
315
|
-
// ✅ Handle index route
|
|
122
|
+
.replace(/\\/g, '/')
|
|
123
|
+
.replace(/\.jux$/i, '')
|
|
124
|
+
.replace(/\./g, '-')
|
|
125
|
+
.replace(/\s+/g, '-')
|
|
126
|
+
.replace(/[^a-z0-9\/_-]/g, '')
|
|
127
|
+
.replace(/-+/g, '-')
|
|
128
|
+
.replace(/^-|-$/g, '');
|
|
129
|
+
|
|
316
130
|
if (routePath === 'index' || routePath === '') {
|
|
317
131
|
routeMap += ` '/': render${cap},\n`;
|
|
318
132
|
}
|
|
319
133
|
|
|
320
|
-
// ✅ Add regular route
|
|
321
134
|
routeMap += ` '/${routePath}': render${cap},\n`;
|
|
322
135
|
});
|
|
323
136
|
|
|
324
|
-
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
137
|
+
// Router setup
|
|
138
|
+
entry += `\n// Router\nconst routes = {\n${routeMap}};\n\n`;
|
|
139
|
+
entry += `function route(path) {\n`;
|
|
140
|
+
entry += ` const renderFn = routes[path] || routes['/'];\n`;
|
|
141
|
+
entry += ` if (renderFn) {\n`;
|
|
142
|
+
entry += ` document.getElementById('app').innerHTML = '';\n`;
|
|
143
|
+
entry += ` renderFn();\n`;
|
|
144
|
+
entry += ` }\n`;
|
|
145
|
+
entry += `}\n\n`;
|
|
146
|
+
entry += `route(window.location.pathname);\n`;
|
|
147
|
+
entry += `window.addEventListener('popstate', () => route(window.location.pathname));\n`;
|
|
148
|
+
|
|
149
|
+
// Save source snapshot
|
|
150
|
+
const snapshotPath = path.join(this.distDir, 'source-snapshot.json');
|
|
151
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(sourceSnapshot, null, 2));
|
|
152
|
+
console.log('📸 Source snapshot written');
|
|
337
153
|
|
|
338
|
-
|
|
339
|
-
var match = stack.match(/render(\\w+)/);
|
|
340
|
-
if (match) {
|
|
341
|
-
var viewName = match[1].toLowerCase();
|
|
342
|
-
for (var file in __juxSources || {}) {
|
|
343
|
-
if (__juxSources[file].name.toLowerCase() === viewName) {
|
|
344
|
-
return { file: file, source: __juxSources[file], viewName: match[1] };
|
|
345
|
-
}
|
|
346
|
-
}
|
|
154
|
+
return entry;
|
|
347
155
|
}
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
156
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
styles: \`
|
|
354
|
-
#__jux-error-overlay {
|
|
355
|
-
position: fixed; inset: 0; z-index: 99999;
|
|
356
|
-
background: rgba(0, 0, 0, 0.4);
|
|
357
|
-
display: flex; align-items: center; justify-content: center;
|
|
358
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
359
|
-
opacity: 0; transition: opacity 0.2s ease-out; backdrop-filter: blur(2px);
|
|
360
|
-
}
|
|
361
|
-
#__jux-error-overlay.visible { opacity: 1; }
|
|
362
|
-
#__jux-error-overlay * { box-sizing: border-box; }
|
|
363
|
-
.__jux-modal {
|
|
364
|
-
background: #f8f9fa; border-radius: 4px;
|
|
365
|
-
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
366
|
-
max-width: 80vw; width: 90%; max-height: 90vh;
|
|
367
|
-
overflow: hidden; display: flex; flex-direction: column;
|
|
368
|
-
transform: translateY(10px); transition: transform 0.2s ease-out;
|
|
369
|
-
}
|
|
370
|
-
#__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
|
|
371
|
-
.__jux-header { background: #fff; padding: 20px 24px; border-bottom: 1px solid #e5e7eb; }
|
|
372
|
-
.__jux-header h3 { margin: 0 0 6px; font-weight: 600; font-size: 11px; color: #9ca3af; text-transform: uppercase; }
|
|
373
|
-
.__jux-header h1 { margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626; line-height: 1.3; }
|
|
374
|
-
.__jux-header .file-info { color: #6b7280; font-size: 13px; }
|
|
375
|
-
.__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
|
|
376
|
-
.__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
|
|
377
|
-
.__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
|
|
378
|
-
.__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
|
|
379
|
-
.__jux-code-line.error { background: #fef2f2; }
|
|
380
|
-
.__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
|
|
381
|
-
.__jux-code-line.context { background: #fefce8; }
|
|
382
|
-
.__jux-line-num { min-width: 44px; padding: 4px 12px; text-align: right; color: #9ca3af; background: #f9fafb; border-right: 1px solid #e5e7eb; font-size: 12px; }
|
|
383
|
-
.__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
|
|
384
|
-
.__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
|
|
385
|
-
.__jux-footer { padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; }
|
|
386
|
-
.__jux-tip { color: #6b7280; font-size: 12px; }
|
|
387
|
-
.__jux-dismiss { background: #f3f4f6; color: #374151; border: 1px solid #d1d5db; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-size: 13px; font-weight: 500; }
|
|
388
|
-
.__jux-dismiss:hover { background: #e5e7eb; }
|
|
389
|
-
.__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
|
|
390
|
-
.__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; }
|
|
391
|
-
\`,
|
|
392
|
-
|
|
393
|
-
show: async function(error, title) {
|
|
394
|
-
title = title || 'Runtime Error';
|
|
395
|
-
var existing = document.getElementById('__jux-error-overlay');
|
|
396
|
-
if (existing) existing.remove();
|
|
397
|
-
await __juxLoadSources();
|
|
398
|
-
|
|
399
|
-
var overlay = document.createElement('div');
|
|
400
|
-
overlay.id = '__jux-error-overlay';
|
|
401
|
-
var stack = error && error.stack ? error.stack : '';
|
|
402
|
-
var msg = error && error.message ? error.message : String(error);
|
|
403
|
-
var found = __juxFindSource(stack);
|
|
404
|
-
var sourceHtml = '', fileInfo = '';
|
|
405
|
-
|
|
406
|
-
if (found && found.source && found.source.lines) {
|
|
407
|
-
var lines = found.source.lines;
|
|
408
|
-
fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
|
|
409
|
-
var errorLineIndex = -1;
|
|
410
|
-
var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
|
|
411
|
-
if (errorMethod) {
|
|
412
|
-
for (var i = 0; i < lines.length; i++) {
|
|
413
|
-
if (lines[i].indexOf('.' + errorMethod[1]) > -1) { errorLineIndex = i; break; }
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (errorLineIndex === -1) {
|
|
417
|
-
for (var i = 0; i < lines.length; i++) {
|
|
418
|
-
if (lines[i].indexOf('throw ') > -1) { errorLineIndex = i; break; }
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
var contextStart = Math.max(0, errorLineIndex - 3);
|
|
422
|
-
var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
|
|
423
|
-
if (errorLineIndex === -1) { contextStart = 0; contextEnd = Math.min(14, lines.length - 1); }
|
|
424
|
-
|
|
425
|
-
for (var i = contextStart; i <= contextEnd; i++) {
|
|
426
|
-
var isError = (i === errorLineIndex);
|
|
427
|
-
var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
|
|
428
|
-
var lineClass = isError ? ' error' : (isContext ? ' context' : '');
|
|
429
|
-
var lineCode = lines[i].replace(/</g, '<').replace(/>/g, '>') || ' ';
|
|
430
|
-
sourceHtml += '<div class="__jux-code-line' + lineClass + '"><span class="__jux-line-num">' + (i + 1) + '</span><span class="__jux-line-code">' + lineCode + '</span></div>';
|
|
431
|
-
}
|
|
432
|
-
} else {
|
|
433
|
-
sourceHtml = '<div class="__jux-no-source"><p>Could not locate source file.</p><pre>' + stack + '</pre></div>';
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
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>';
|
|
437
|
-
document.body.appendChild(overlay);
|
|
438
|
-
requestAnimationFrame(function() { overlay.classList.add('visible'); });
|
|
439
|
-
console.error(title + ':', error);
|
|
440
|
-
}
|
|
441
|
-
};
|
|
442
|
-
|
|
443
|
-
window.addEventListener('error', function(e) { __juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error'); }, true);
|
|
444
|
-
window.addEventListener('unhandledrejection', function(e) { __juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection'); }, true);
|
|
445
|
-
|
|
446
|
-
// --- JUX ROUTER ---
|
|
447
|
-
const routes = {\n${routeMap}};
|
|
448
|
-
|
|
449
|
-
// Simple router
|
|
450
|
-
function route(path) {
|
|
451
|
-
const renderFn = routes[path] || routes['/'];
|
|
452
|
-
if (renderFn) {
|
|
453
|
-
document.getElementById('app').innerHTML = '';
|
|
454
|
-
renderFn();
|
|
455
|
-
} else {
|
|
456
|
-
document.getElementById('app').innerHTML = '<h1>404 - Page Not Found</h1>';
|
|
157
|
+
removeImports(code) {
|
|
158
|
+
return code.replace(/^\s*import\s+.*?from\s+['"].*?['"];?\s*$/gm, '');
|
|
457
159
|
}
|
|
458
|
-
}
|
|
459
160
|
|
|
460
|
-
|
|
461
|
-
|
|
161
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
162
|
+
* COPY PUBLIC ASSETS
|
|
163
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
462
164
|
|
|
463
|
-
|
|
464
|
-
|
|
165
|
+
async copyPublicAssets() {
|
|
166
|
+
const publicPath = this.paths?.public || path.resolve(this.srcDir, '..', this.publicDir);
|
|
465
167
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
const href = e.target.getAttribute('href');
|
|
470
|
-
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
471
|
-
e.preventDefault();
|
|
472
|
-
window.history.pushState({}, '', href);
|
|
473
|
-
route(href);
|
|
168
|
+
if (!fs.existsSync(publicPath)) {
|
|
169
|
+
console.log(`ℹ️ No public folder found, skipping`);
|
|
170
|
+
return;
|
|
474
171
|
}
|
|
172
|
+
|
|
173
|
+
console.log('📦 Copying public assets...');
|
|
174
|
+
|
|
175
|
+
const copyRecursive = (src, dest) => {
|
|
176
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
177
|
+
|
|
178
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
179
|
+
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const srcPath = path.join(src, entry.name);
|
|
182
|
+
const destPath = path.join(dest, entry.name);
|
|
183
|
+
|
|
184
|
+
if (entry.isDirectory()) {
|
|
185
|
+
copyRecursive(srcPath, destPath);
|
|
186
|
+
} else {
|
|
187
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
188
|
+
// ✅ Skip .jux files (they're compiled, not copied)
|
|
189
|
+
if (ext !== '.jux') {
|
|
190
|
+
fs.copyFileSync(srcPath, destPath);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
copyRecursive(publicPath, this.distDir);
|
|
197
|
+
console.log('✅ Public assets copied');
|
|
475
198
|
}
|
|
476
|
-
|
|
477
|
-
|
|
199
|
+
|
|
200
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
201
|
+
* GENERATE index.html
|
|
202
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
203
|
+
|
|
204
|
+
async generateIndexHtml() {
|
|
205
|
+
console.log(' ✅ Generated index.html');
|
|
206
|
+
|
|
207
|
+
const html = `<!DOCTYPE html>
|
|
208
|
+
<html lang="en">
|
|
209
|
+
<head>
|
|
210
|
+
<meta charset="UTF-8">
|
|
211
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
212
|
+
<title>JUX Application</title>
|
|
213
|
+
<link rel="stylesheet" href="/styles.css">
|
|
214
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
215
|
+
</head>
|
|
216
|
+
<body>
|
|
217
|
+
<div id="app"></div>
|
|
218
|
+
<script src="/entry.js"></script>
|
|
219
|
+
</body>
|
|
220
|
+
</html>`;
|
|
221
|
+
|
|
222
|
+
fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
|
|
478
223
|
}
|
|
479
224
|
|
|
480
|
-
|
|
481
|
-
|
|
225
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
226
|
+
* BUILD
|
|
227
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
482
228
|
|
|
229
|
+
async build() {
|
|
483
230
|
try {
|
|
484
|
-
//
|
|
485
|
-
if (fs.existsSync(this.
|
|
486
|
-
fs.rmSync(this.
|
|
231
|
+
// Clean dist
|
|
232
|
+
if (fs.existsSync(this.distDir)) {
|
|
233
|
+
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
487
234
|
}
|
|
488
|
-
fs.mkdirSync(this.
|
|
235
|
+
fs.mkdirSync(this.distDir, { recursive: true });
|
|
489
236
|
|
|
490
|
-
//
|
|
491
|
-
|
|
492
|
-
console.log(`📂 Found ${juxFiles.length} .jux files\n`);
|
|
237
|
+
// Copy public assets first
|
|
238
|
+
await this.copyPublicAssets();
|
|
493
239
|
|
|
494
|
-
//
|
|
495
|
-
|
|
240
|
+
// Scan files
|
|
241
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
242
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
243
|
+
|
|
244
|
+
// Generate entry point
|
|
245
|
+
const entryCode = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
246
|
+
const entryPath = path.join(this.distDir, 'entry-temp.js');
|
|
247
|
+
fs.writeFileSync(entryPath, entryCode, 'utf8');
|
|
248
|
+
|
|
249
|
+
// Bundle with esbuild
|
|
250
|
+
await esbuild.build({
|
|
251
|
+
entryPoints: [entryPath],
|
|
252
|
+
bundle: true,
|
|
253
|
+
outfile: path.join(this.distDir, 'entry.js'),
|
|
254
|
+
format: 'esm',
|
|
255
|
+
platform: 'browser',
|
|
256
|
+
target: 'es2020',
|
|
257
|
+
minify: false,
|
|
258
|
+
sourcemap: false
|
|
259
|
+
});
|
|
496
260
|
|
|
497
|
-
|
|
498
|
-
await this.bundleVendorFiles();
|
|
261
|
+
console.log('✅ esbuild complete');
|
|
499
262
|
|
|
500
|
-
//
|
|
501
|
-
|
|
263
|
+
// Clean up temp file
|
|
264
|
+
fs.unlinkSync(entryPath);
|
|
502
265
|
|
|
503
|
-
//
|
|
266
|
+
// Generate index.html
|
|
504
267
|
await this.generateIndexHtml();
|
|
505
268
|
|
|
506
|
-
|
|
507
|
-
console.log(`📁 Output: ${this.config.distDir}`);
|
|
508
|
-
console.log(` ✓ entry.js (${juxFiles.length} .jux files bundled)`);
|
|
509
|
-
console.log(` ✓ bundle.js (vendor libraries)`);
|
|
510
|
-
console.log(` ✓ index.html`);
|
|
511
|
-
console.log(` ✓ Public assets copied\n`);
|
|
512
|
-
|
|
513
|
-
// ✅ Consistent return object
|
|
269
|
+
// ✅ Return success object
|
|
514
270
|
return {
|
|
515
271
|
success: true,
|
|
516
272
|
errors: [],
|
|
517
273
|
warnings: [],
|
|
518
|
-
fileCount:
|
|
274
|
+
fileCount: views.length
|
|
519
275
|
};
|
|
520
276
|
|
|
521
277
|
} catch (error) {
|
|
@@ -530,98 +286,4 @@ document.addEventListener('click', (e) => {
|
|
|
530
286
|
};
|
|
531
287
|
}
|
|
532
288
|
}
|
|
533
|
-
|
|
534
|
-
/**
|
|
535
|
-
* Copy public folder contents to dist
|
|
536
|
-
*/
|
|
537
|
-
copyPublicFolder() {
|
|
538
|
-
// ✅ Use configured public path or resolve from paths object
|
|
539
|
-
const publicSrc = this.paths.public
|
|
540
|
-
? this.paths.public
|
|
541
|
-
: path.resolve(process.cwd(), this.publicDir);
|
|
542
|
-
|
|
543
|
-
if (!fs.existsSync(publicSrc)) {
|
|
544
|
-
return; // No public folder, skip
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
console.log('📦 Copying public assets...');
|
|
548
|
-
|
|
549
|
-
try {
|
|
550
|
-
this._copyDirRecursive(publicSrc, this.distDir, 0);
|
|
551
|
-
console.log('✅ Public assets copied');
|
|
552
|
-
} catch (err) {
|
|
553
|
-
console.warn('⚠️ Error copying public folder:', err.message);
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
/**
|
|
558
|
-
* Recursively copy directory contents
|
|
559
|
-
*/
|
|
560
|
-
_copyDirRecursive(src, dest, depth = 0) {
|
|
561
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
562
|
-
|
|
563
|
-
entries.forEach(entry => {
|
|
564
|
-
// Skip hidden files and directories
|
|
565
|
-
if (entry.name.startsWith('.')) return;
|
|
566
|
-
|
|
567
|
-
const srcPath = path.join(src, entry.name);
|
|
568
|
-
const destPath = path.join(dest, entry.name);
|
|
569
|
-
|
|
570
|
-
if (entry.isDirectory()) {
|
|
571
|
-
// Create directory and recurse
|
|
572
|
-
if (!fs.existsSync(destPath)) {
|
|
573
|
-
fs.mkdirSync(destPath, { recursive: true });
|
|
574
|
-
}
|
|
575
|
-
this._copyDirRecursive(srcPath, destPath, depth + 1);
|
|
576
|
-
} else {
|
|
577
|
-
// Copy file
|
|
578
|
-
fs.copyFileSync(srcPath, destPath);
|
|
579
|
-
|
|
580
|
-
// Log files at root level only
|
|
581
|
-
if (depth === 0) {
|
|
582
|
-
const ext = path.extname(entry.name);
|
|
583
|
-
const icon = this._getFileIcon(ext);
|
|
584
|
-
console.log(` ${icon} ${entry.name}`);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
});
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
/**
|
|
591
|
-
* Get icon for file type
|
|
592
|
-
*/
|
|
593
|
-
_getFileIcon(ext) {
|
|
594
|
-
const icons = {
|
|
595
|
-
'.html': '📄',
|
|
596
|
-
'.css': '🎨',
|
|
597
|
-
'.js': '📜',
|
|
598
|
-
'.json': '📋',
|
|
599
|
-
'.png': '🖼️',
|
|
600
|
-
'.jpg': '🖼️',
|
|
601
|
-
'.jpeg': '🖼️',
|
|
602
|
-
'.gif': '🖼️',
|
|
603
|
-
'.svg': '🎨',
|
|
604
|
-
'.ico': '🔖',
|
|
605
|
-
'.woff': '🔤',
|
|
606
|
-
'.woff2': '🔤',
|
|
607
|
-
'.ttf': '🔤',
|
|
608
|
-
'.eot': '🔤'
|
|
609
|
-
};
|
|
610
|
-
return icons[ext.toLowerCase()] || '📦';
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* ✅ Generate valid JavaScript identifier from file path
|
|
615
|
-
* Example: abc/aaa.jux -> abc_aaa
|
|
616
|
-
* Example: menus/main.jux -> menus_main
|
|
617
|
-
* Example: pages/blog/post.jux -> pages_blog_post
|
|
618
|
-
*/
|
|
619
|
-
_generateNameFromPath(filepath) {
|
|
620
|
-
return filepath
|
|
621
|
-
.replace(/\.jux$/, '') // Remove .jux extension
|
|
622
|
-
.replace(/[\/\\]/g, '_') // Replace / and \ with _
|
|
623
|
-
.replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
|
|
624
|
-
.replace(/_+/g, '_') // Collapse multiple consecutive underscores
|
|
625
|
-
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
|
|
626
|
-
}
|
|
627
289
|
}
|