juxscript 1.1.129 → 1.1.131
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 +524 -149
- package/package.json +1 -1
package/dom-structure-map.json
CHANGED
package/machinery/compiler3.js
CHANGED
|
@@ -1,25 +1,66 @@
|
|
|
1
|
+
import * as esbuild from 'esbuild';
|
|
2
|
+
import * as acorn from 'acorn';
|
|
3
|
+
import { walk } from 'astray';
|
|
1
4
|
import fs from 'fs';
|
|
2
5
|
import path from 'path';
|
|
3
|
-
import
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
4
10
|
|
|
5
11
|
export class JuxCompiler {
|
|
6
|
-
constructor(config) {
|
|
12
|
+
constructor(config = {}) {
|
|
7
13
|
this.config = config;
|
|
8
|
-
this.srcDir = config.srcDir;
|
|
9
|
-
this.distDir = config.distDir;
|
|
10
|
-
this.publicDir = config.publicDir;
|
|
11
|
-
this.
|
|
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;
|
|
12
21
|
}
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
*
|
|
16
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Locate juxscript package - simplified resolution
|
|
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
|
+
}
|
|
17
55
|
|
|
56
|
+
/**
|
|
57
|
+
* ✅ Recursively scan for .jux and .js files in srcDir and subdirectories
|
|
58
|
+
*/
|
|
18
59
|
scanFiles() {
|
|
19
60
|
const views = [], dataModules = [], sharedModules = [];
|
|
20
61
|
|
|
21
62
|
/**
|
|
22
|
-
*
|
|
63
|
+
* Recursive directory scanner
|
|
23
64
|
*/
|
|
24
65
|
const scanDirectory = (currentDir) => {
|
|
25
66
|
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
@@ -58,37 +99,146 @@ export class JuxCompiler {
|
|
|
58
99
|
}
|
|
59
100
|
|
|
60
101
|
isAssetFile(filename) {
|
|
61
|
-
const assetExtensions = ['.css', '.
|
|
102
|
+
const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
|
|
62
103
|
return assetExtensions.some(ext => filename.endsWith(ext));
|
|
63
104
|
}
|
|
64
105
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
106
|
+
removeImports(code) {
|
|
107
|
+
return code
|
|
108
|
+
.replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
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;
|
|
68
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
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generate entry point without layout/theme logic
|
|
198
|
+
*/
|
|
69
199
|
generateEntryPoint(views, dataModules, sharedModules) {
|
|
200
|
+
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
201
|
+
const allIssues = [];
|
|
70
202
|
const sourceSnapshot = {};
|
|
71
|
-
let entry = `/* Auto-generated entry point */\n`;
|
|
72
|
-
entry += `import { jux, state, registry } from 'juxscript';\n\n`;
|
|
73
203
|
|
|
74
|
-
|
|
75
|
-
sharedModules.forEach(m => {
|
|
76
|
-
|
|
204
|
+
const juxImports = new Set();
|
|
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
|
+
}
|
|
77
211
|
});
|
|
78
212
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
+
});
|
|
220
|
+
sharedModules.forEach(m => {
|
|
221
|
+
entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
|
|
82
222
|
});
|
|
83
223
|
|
|
84
|
-
|
|
224
|
+
entry += `\n// Expose to window\n`;
|
|
225
|
+
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
226
|
+
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
227
|
+
|
|
228
|
+
if (juxImports.size > 0) {
|
|
229
|
+
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
233
|
+
|
|
85
234
|
views.forEach(v => {
|
|
86
235
|
// ✅ Sanitize the name for use in function names
|
|
236
|
+
// Replace slashes, dots, and other invalid characters with underscores
|
|
87
237
|
const sanitizedName = v.name
|
|
88
|
-
.replace(/[\/\\.\\-\s]/g, '_')
|
|
89
|
-
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
90
|
-
.replace(/_+/g, '_')
|
|
91
|
-
.replace(/^_|_$/g, '');
|
|
238
|
+
.replace(/[\/\\.\\-\s]/g, '_') // Replace /, \, ., -, spaces with _
|
|
239
|
+
.replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
|
|
240
|
+
.replace(/_+/g, '_') // Collapse multiple consecutive underscores
|
|
241
|
+
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
|
|
92
242
|
|
|
93
243
|
const capitalized = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
|
|
94
244
|
|
|
@@ -102,12 +252,44 @@ export class JuxCompiler {
|
|
|
102
252
|
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
103
253
|
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
104
254
|
|
|
255
|
+
// ✅ Use sanitized name in function declaration
|
|
105
256
|
entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
|
|
106
257
|
});
|
|
107
258
|
|
|
108
|
-
|
|
259
|
+
dataModules.forEach(m => {
|
|
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) {
|
|
109
290
|
let routeMap = '';
|
|
110
291
|
views.forEach(v => {
|
|
292
|
+
// ✅ Sanitize function name (same as above)
|
|
111
293
|
const sanitizedName = v.name
|
|
112
294
|
.replace(/[\/\\.\\-\s]/g, '_')
|
|
113
295
|
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
@@ -116,93 +298,251 @@ export class JuxCompiler {
|
|
|
116
298
|
|
|
117
299
|
const cap = sanitizedName.charAt(0).toUpperCase() + sanitizedName.slice(1);
|
|
118
300
|
|
|
119
|
-
// ✅ Generate URL-safe route path
|
|
301
|
+
// ✅ Generate URL-safe route path from original name
|
|
302
|
+
// Convert: 'menus/main' → '/menus/main'
|
|
303
|
+
// Convert: 'about-us' → '/about-us'
|
|
304
|
+
// Convert: 'blog.post' → '/blog-post' (dots become dashes for URLs)
|
|
120
305
|
const routePath = v.name
|
|
121
306
|
.toLowerCase()
|
|
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
|
-
|
|
307
|
+
.replace(/\\/g, '/') // Normalize backslashes to forward slashes
|
|
308
|
+
.replace(/\.jux$/i, '') // Remove .jux extension if present
|
|
309
|
+
.replace(/\./g, '-') // Convert dots to dashes (blog.post → blog-post)
|
|
310
|
+
.replace(/\s+/g, '-') // Convert spaces to dashes
|
|
311
|
+
.replace(/[^a-z0-9\/_-]/g, '') // Remove any other unsafe URL chars
|
|
312
|
+
.replace(/-+/g, '-') // Collapse multiple dashes
|
|
313
|
+
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
|
314
|
+
|
|
315
|
+
// ✅ Handle index route
|
|
130
316
|
if (routePath === 'index' || routePath === '') {
|
|
131
317
|
routeMap += ` '/': render${cap},\n`;
|
|
132
318
|
}
|
|
133
319
|
|
|
320
|
+
// ✅ Add regular route
|
|
134
321
|
routeMap += ` '/${routePath}': render${cap},\n`;
|
|
135
322
|
});
|
|
136
323
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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');
|
|
324
|
+
return `
|
|
325
|
+
// --- JUX SOURCE LOADER ---
|
|
326
|
+
var __juxSources = null;
|
|
327
|
+
async function __juxLoadSources() {
|
|
328
|
+
if (__juxSources) return __juxSources;
|
|
329
|
+
try {
|
|
330
|
+
var res = await fetch('/__jux_sources.json');
|
|
331
|
+
__juxSources = await res.json();
|
|
332
|
+
} catch (e) {
|
|
333
|
+
__juxSources = {};
|
|
334
|
+
}
|
|
335
|
+
return __juxSources;
|
|
336
|
+
}
|
|
153
337
|
|
|
154
|
-
|
|
338
|
+
function __juxFindSource(stack) {
|
|
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
|
+
}
|
|
155
347
|
}
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
156
350
|
|
|
157
|
-
|
|
158
|
-
|
|
351
|
+
// --- JUX RUNTIME ERROR OVERLAY ---
|
|
352
|
+
var __juxErrorOverlay = {
|
|
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);
|
|
159
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>';
|
|
457
|
+
}
|
|
458
|
+
}
|
|
160
459
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
460
|
+
// Initial route
|
|
461
|
+
route(window.location.pathname);
|
|
164
462
|
|
|
165
|
-
|
|
166
|
-
|
|
463
|
+
// Handle navigation
|
|
464
|
+
window.addEventListener('popstate', () => route(window.location.pathname));
|
|
167
465
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
466
|
+
// Intercept link clicks
|
|
467
|
+
document.addEventListener('click', (e) => {
|
|
468
|
+
if (e.target.matches('a[href]')) {
|
|
469
|
+
const href = e.target.getAttribute('href');
|
|
470
|
+
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
471
|
+
e.preventDefault();
|
|
472
|
+
window.history.pushState({}, '', href);
|
|
473
|
+
route(href);
|
|
171
474
|
}
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
`;
|
|
478
|
+
}
|
|
172
479
|
|
|
173
|
-
|
|
480
|
+
async build() {
|
|
481
|
+
console.log('🚀 JUX Build\n');
|
|
174
482
|
|
|
175
|
-
const
|
|
176
|
-
|
|
483
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
484
|
+
if (!juxscriptPath) {
|
|
485
|
+
console.error('❌ Could not locate juxscript package');
|
|
486
|
+
return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
|
|
487
|
+
}
|
|
488
|
+
console.log(`📦 Using: ${juxscriptPath}`);
|
|
177
489
|
|
|
178
|
-
|
|
490
|
+
await this.loadJuxscriptExports();
|
|
179
491
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
492
|
+
if (fs.existsSync(this.distDir)) {
|
|
493
|
+
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
494
|
+
}
|
|
495
|
+
fs.mkdirSync(this.distDir, { recursive: true });
|
|
183
496
|
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
};
|
|
497
|
+
// ✅ Copy public folder if exists
|
|
498
|
+
this.copyPublicFolder();
|
|
195
499
|
|
|
196
|
-
|
|
197
|
-
console.log(
|
|
198
|
-
|
|
500
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
501
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
502
|
+
|
|
503
|
+
// Copy data/shared modules to dist
|
|
504
|
+
const juxDistDir = path.join(this.distDir, 'jux');
|
|
505
|
+
fs.mkdirSync(juxDistDir, { recursive: true });
|
|
506
|
+
[...dataModules, ...sharedModules].forEach(m => {
|
|
507
|
+
fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
|
|
508
|
+
});
|
|
199
509
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
510
|
+
const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
511
|
+
const entryPath = path.join(this.distDir, 'entry.js');
|
|
512
|
+
fs.writeFileSync(entryPath, entryContent);
|
|
203
513
|
|
|
204
|
-
|
|
205
|
-
|
|
514
|
+
const snapshotPath = path.join(this.distDir, '__jux_sources.json');
|
|
515
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
516
|
+
console.log(`📸 Source snapshot written`);
|
|
517
|
+
|
|
518
|
+
const validation = this.reportValidationIssues();
|
|
519
|
+
if (!validation.isValid) {
|
|
520
|
+
console.log('🛑 BUILD FAILED\n');
|
|
521
|
+
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
await esbuild.build({
|
|
526
|
+
entryPoints: [entryPath],
|
|
527
|
+
bundle: true,
|
|
528
|
+
outfile: path.join(this.distDir, 'bundle.js'),
|
|
529
|
+
format: 'esm',
|
|
530
|
+
platform: 'browser',
|
|
531
|
+
target: 'esnext',
|
|
532
|
+
sourcemap: true,
|
|
533
|
+
loader: { '.jux': 'js', '.css': 'empty' },
|
|
534
|
+
plugins: [{
|
|
535
|
+
name: 'juxscript-resolver',
|
|
536
|
+
setup: (build) => {
|
|
537
|
+
build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
|
|
538
|
+
}
|
|
539
|
+
}],
|
|
540
|
+
});
|
|
541
|
+
console.log('✅ esbuild complete');
|
|
542
|
+
} catch (err) {
|
|
543
|
+
console.error('❌ esbuild failed:', err);
|
|
544
|
+
return { success: false, errors: [{ message: err.message }], warnings: [] };
|
|
545
|
+
}
|
|
206
546
|
|
|
207
547
|
const html = `<!DOCTYPE html>
|
|
208
548
|
<html lang="en">
|
|
@@ -210,80 +550,115 @@ export class JuxCompiler {
|
|
|
210
550
|
<meta charset="UTF-8">
|
|
211
551
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
212
552
|
<title>JUX Application</title>
|
|
213
|
-
<
|
|
214
|
-
<script src="
|
|
553
|
+
<script type="module" src="/bundle.js"></script>
|
|
554
|
+
<script src="/entry.js"></script>
|
|
215
555
|
</head>
|
|
216
556
|
<body>
|
|
217
557
|
<div id="app"></div>
|
|
218
|
-
<script src="/entry.js"></script>
|
|
219
558
|
</body>
|
|
220
559
|
</html>`;
|
|
221
560
|
|
|
222
|
-
fs.writeFileSync(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
* ═══════════════════════════════════════════════════════════════════ */
|
|
228
|
-
|
|
229
|
-
async build() {
|
|
230
|
-
try {
|
|
231
|
-
// Clean dist
|
|
232
|
-
if (fs.existsSync(this.distDir)) {
|
|
233
|
-
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
234
|
-
}
|
|
235
|
-
fs.mkdirSync(this.distDir, { recursive: true });
|
|
561
|
+
fs.writeFileSync(
|
|
562
|
+
path.join(this.config.distDir, 'index.html'),
|
|
563
|
+
html,
|
|
564
|
+
'utf8'
|
|
565
|
+
);
|
|
236
566
|
|
|
237
|
-
|
|
238
|
-
|
|
567
|
+
console.log(' ✅ Generated index.html');
|
|
568
|
+
return { success: true, errors: [], warnings: validation.warnings };
|
|
569
|
+
}
|
|
239
570
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
571
|
+
/**
|
|
572
|
+
* Copy public folder contents to dist
|
|
573
|
+
*/
|
|
574
|
+
copyPublicFolder() {
|
|
575
|
+
// ✅ Use configured public path or resolve from paths object
|
|
576
|
+
const publicSrc = this.paths.public
|
|
577
|
+
? this.paths.public
|
|
578
|
+
: path.resolve(process.cwd(), this.publicDir);
|
|
579
|
+
|
|
580
|
+
if (!fs.existsSync(publicSrc)) {
|
|
581
|
+
return; // No public folder, skip
|
|
582
|
+
}
|
|
243
583
|
|
|
244
|
-
|
|
245
|
-
const entryCode = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
246
|
-
const entryPath = path.join(this.distDir, 'entry-temp.js');
|
|
247
|
-
fs.writeFileSync(entryPath, entryCode, 'utf8');
|
|
584
|
+
console.log('📦 Copying public assets...');
|
|
248
585
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
target: 'es2020',
|
|
257
|
-
minify: false,
|
|
258
|
-
sourcemap: false
|
|
259
|
-
});
|
|
586
|
+
try {
|
|
587
|
+
this._copyDirRecursive(publicSrc, this.distDir, 0);
|
|
588
|
+
console.log('✅ Public assets copied');
|
|
589
|
+
} catch (err) {
|
|
590
|
+
console.warn('⚠️ Error copying public folder:', err.message);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
260
593
|
|
|
261
|
-
|
|
594
|
+
/**
|
|
595
|
+
* Recursively copy directory contents
|
|
596
|
+
*/
|
|
597
|
+
_copyDirRecursive(src, dest, depth = 0) {
|
|
598
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
262
599
|
|
|
263
|
-
|
|
264
|
-
|
|
600
|
+
entries.forEach(entry => {
|
|
601
|
+
// Skip hidden files and directories
|
|
602
|
+
if (entry.name.startsWith('.')) return;
|
|
265
603
|
|
|
266
|
-
|
|
267
|
-
|
|
604
|
+
const srcPath = path.join(src, entry.name);
|
|
605
|
+
const destPath = path.join(dest, entry.name);
|
|
268
606
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
}
|
|
607
|
+
if (entry.isDirectory()) {
|
|
608
|
+
// Create directory and recurse
|
|
609
|
+
if (!fs.existsSync(destPath)) {
|
|
610
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
611
|
+
}
|
|
612
|
+
this._copyDirRecursive(srcPath, destPath, depth + 1);
|
|
613
|
+
} else {
|
|
614
|
+
// Copy file
|
|
615
|
+
fs.copyFileSync(srcPath, destPath);
|
|
616
|
+
|
|
617
|
+
// Log files at root level only
|
|
618
|
+
if (depth === 0) {
|
|
619
|
+
const ext = path.extname(entry.name);
|
|
620
|
+
const icon = this._getFileIcon(ext);
|
|
621
|
+
console.log(` ${icon} ${entry.name}`);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
276
626
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
627
|
+
/**
|
|
628
|
+
* Get icon for file type
|
|
629
|
+
*/
|
|
630
|
+
_getFileIcon(ext) {
|
|
631
|
+
const icons = {
|
|
632
|
+
'.html': '📄',
|
|
633
|
+
'.css': '🎨',
|
|
634
|
+
'.js': '📜',
|
|
635
|
+
'.json': '📋',
|
|
636
|
+
'.png': '🖼️',
|
|
637
|
+
'.jpg': '🖼️',
|
|
638
|
+
'.jpeg': '🖼️',
|
|
639
|
+
'.gif': '🖼️',
|
|
640
|
+
'.svg': '🎨',
|
|
641
|
+
'.ico': '🔖',
|
|
642
|
+
'.woff': '🔤',
|
|
643
|
+
'.woff2': '🔤',
|
|
644
|
+
'.ttf': '🔤',
|
|
645
|
+
'.eot': '🔤'
|
|
646
|
+
};
|
|
647
|
+
return icons[ext.toLowerCase()] || '📦';
|
|
648
|
+
}
|
|
280
649
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
650
|
+
/**
|
|
651
|
+
* ✅ Generate valid JavaScript identifier from file path
|
|
652
|
+
* Example: abc/aaa.jux -> abc_aaa
|
|
653
|
+
* Example: menus/main.jux -> menus_main
|
|
654
|
+
* Example: pages/blog/post.jux -> pages_blog_post
|
|
655
|
+
*/
|
|
656
|
+
_generateNameFromPath(filepath) {
|
|
657
|
+
return filepath
|
|
658
|
+
.replace(/\.jux$/, '') // Remove .jux extension
|
|
659
|
+
.replace(/[\/\\]/g, '_') // Replace / and \ with _
|
|
660
|
+
.replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
|
|
661
|
+
.replace(/_+/g, '_') // Collapse multiple consecutive underscores
|
|
662
|
+
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
|
|
288
663
|
}
|
|
289
664
|
}
|