juxscript 1.1.127 → 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 +149 -524
- 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,251 +116,93 @@ 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
|
-
|
|
460
|
-
// Initial route
|
|
461
|
-
route(window.location.pathname);
|
|
462
160
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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);
|
|
474
|
-
}
|
|
475
|
-
}
|
|
476
|
-
});
|
|
477
|
-
`;
|
|
478
|
-
}
|
|
161
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
162
|
+
* COPY PUBLIC ASSETS
|
|
163
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
479
164
|
|
|
480
|
-
async
|
|
481
|
-
|
|
165
|
+
async copyPublicAssets() {
|
|
166
|
+
const publicPath = this.paths?.public || path.resolve(this.srcDir, '..', this.publicDir);
|
|
482
167
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
|
|
168
|
+
if (!fs.existsSync(publicPath)) {
|
|
169
|
+
console.log(`ℹ️ No public folder found, skipping`);
|
|
170
|
+
return;
|
|
487
171
|
}
|
|
488
|
-
console.log(`📦 Using: ${juxscriptPath}`);
|
|
489
|
-
|
|
490
|
-
await this.loadJuxscriptExports();
|
|
491
172
|
|
|
492
|
-
|
|
493
|
-
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
494
|
-
}
|
|
495
|
-
fs.mkdirSync(this.distDir, { recursive: true });
|
|
173
|
+
console.log('📦 Copying public assets...');
|
|
496
174
|
|
|
497
|
-
|
|
498
|
-
|
|
175
|
+
const copyRecursive = (src, dest) => {
|
|
176
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
499
177
|
|
|
500
|
-
|
|
501
|
-
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
178
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
502
179
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
[...dataModules, ...sharedModules].forEach(m => {
|
|
507
|
-
fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
|
|
508
|
-
});
|
|
180
|
+
for (const entry of entries) {
|
|
181
|
+
const srcPath = path.join(src, entry.name);
|
|
182
|
+
const destPath = path.join(dest, entry.name);
|
|
509
183
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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
|
+
};
|
|
513
195
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
196
|
+
copyRecursive(publicPath, this.distDir);
|
|
197
|
+
console.log('✅ Public assets copied');
|
|
198
|
+
}
|
|
517
199
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
522
|
-
}
|
|
200
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
201
|
+
* GENERATE index.html
|
|
202
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
523
203
|
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
}
|
|
204
|
+
async generateIndexHtml() {
|
|
205
|
+
console.log(' ✅ Generated index.html');
|
|
546
206
|
|
|
547
207
|
const html = `<!DOCTYPE html>
|
|
548
208
|
<html lang="en">
|
|
@@ -550,115 +210,80 @@ document.addEventListener('click', (e) => {
|
|
|
550
210
|
<meta charset="UTF-8">
|
|
551
211
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
552
212
|
<title>JUX Application</title>
|
|
553
|
-
<
|
|
554
|
-
<script src="
|
|
213
|
+
<link rel="stylesheet" href="/styles.css">
|
|
214
|
+
<script src="https://unpkg.com/lucide@latest"></script>
|
|
555
215
|
</head>
|
|
556
216
|
<body>
|
|
557
217
|
<div id="app"></div>
|
|
218
|
+
<script src="/entry.js"></script>
|
|
558
219
|
</body>
|
|
559
220
|
</html>`;
|
|
560
221
|
|
|
561
|
-
fs.writeFileSync(
|
|
562
|
-
path.join(this.config.distDir, 'index.html'),
|
|
563
|
-
html,
|
|
564
|
-
'utf8'
|
|
565
|
-
);
|
|
566
|
-
|
|
567
|
-
console.log(' ✅ Generated index.html');
|
|
568
|
-
return { success: true, errors: [], warnings: validation.warnings };
|
|
222
|
+
fs.writeFileSync(path.join(this.distDir, 'index.html'), html, 'utf8');
|
|
569
223
|
}
|
|
570
224
|
|
|
571
|
-
|
|
572
|
-
*
|
|
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
|
-
}
|
|
583
|
-
|
|
584
|
-
console.log('📦 Copying public assets...');
|
|
225
|
+
/* ═══════════════════════════════════════════════════════════════════
|
|
226
|
+
* BUILD
|
|
227
|
+
* ═══════════════════════════════════════════════════════════════════ */
|
|
585
228
|
|
|
229
|
+
async build() {
|
|
586
230
|
try {
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
}
|
|
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 });
|
|
593
236
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
*/
|
|
597
|
-
_copyDirRecursive(src, dest, depth = 0) {
|
|
598
|
-
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
237
|
+
// Copy public assets first
|
|
238
|
+
await this.copyPublicAssets();
|
|
599
239
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
240
|
+
// Scan files
|
|
241
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
242
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
603
243
|
|
|
604
|
-
|
|
605
|
-
const
|
|
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');
|
|
606
248
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
}
|
|
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
|
+
});
|
|
626
260
|
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
}
|
|
261
|
+
console.log('✅ esbuild complete');
|
|
262
|
+
|
|
263
|
+
// Clean up temp file
|
|
264
|
+
fs.unlinkSync(entryPath);
|
|
649
265
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
.
|
|
266
|
+
// Generate index.html
|
|
267
|
+
await this.generateIndexHtml();
|
|
268
|
+
|
|
269
|
+
// ✅ Return success object
|
|
270
|
+
return {
|
|
271
|
+
success: true,
|
|
272
|
+
errors: [],
|
|
273
|
+
warnings: [],
|
|
274
|
+
fileCount: views.length
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error('\n❌ Build failed:', error.message);
|
|
279
|
+
console.error(error.stack);
|
|
280
|
+
|
|
281
|
+
// ✅ Return failure object
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
errors: [error.message],
|
|
285
|
+
warnings: []
|
|
286
|
+
};
|
|
287
|
+
}
|
|
663
288
|
}
|
|
664
289
|
}
|