juxscript 1.0.89 ā 1.0.91
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/bin/cli.js +243 -92
- package/index.js +22 -0
- package/lib/globals.d.ts +7 -5
- package/machinery/build3.js +21 -0
- package/machinery/compiler3.js +638 -0
- package/machinery/serve.js +255 -0
- package/machinery/watcher.js +53 -64
- package/package.json +11 -3
- package/types/css.d.ts +8 -0
- package/lib/componentsv2/index.d.ts +0 -39
- package/lib/componentsv2/index.d.ts.map +0 -1
- package/lib/componentsv2/index.js +0 -221
- package/lib/componentsv2/index.js.map +0 -1
- package/lib/componentsv2/index.ts +0 -253
- package/machinery/ast.js +0 -347
- package/machinery/compiler.js +0 -706
- package/machinery/diagnose.js +0 -72
- package/machinery/doc-generator.js +0 -136
- package/machinery/imports.js +0 -155
- package/machinery/jux-module-pattern.md +0 -118
- package/machinery/server.js +0 -86
- package/machinery/ts-shim.js +0 -46
- package/machinery/verifier.js +0 -135
- package/tests/dropdown-test.js +0 -25
- package/tests/juxerrors/bad_syntax.jux +0 -8
- package/tests/juxerrors/ghost_dep.jux +0 -10
- package/tests/server_plugin_test.ts +0 -191
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import * as esbuild from 'esbuild';
|
|
2
|
+
import * as acorn from 'acorn';
|
|
3
|
+
import { walk } from 'astray';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
9
|
+
const __dirname = path.dirname(__filename);
|
|
10
|
+
|
|
11
|
+
export class JuxCompiler {
|
|
12
|
+
constructor(config = {}) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
this.srcDir = config.srcDir || './jux';
|
|
15
|
+
this.distDir = config.distDir || './.jux-dist';
|
|
16
|
+
this._juxscriptExports = null;
|
|
17
|
+
this._juxscriptPath = null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Locate juxscript package - simplified resolution
|
|
22
|
+
*/
|
|
23
|
+
findJuxscriptPath() {
|
|
24
|
+
if (this._juxscriptPath) return this._juxscriptPath;
|
|
25
|
+
|
|
26
|
+
const projectRoot = process.cwd();
|
|
27
|
+
|
|
28
|
+
// Priority 1: User's node_modules (when used as dependency)
|
|
29
|
+
const userPath = path.resolve(projectRoot, 'node_modules/juxscript/index.js');
|
|
30
|
+
if (fs.existsSync(userPath)) {
|
|
31
|
+
this._juxscriptPath = userPath;
|
|
32
|
+
return userPath;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Priority 2: Package root (when developing juxscript itself)
|
|
36
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
37
|
+
const devPath = path.resolve(packageRoot, 'index.js');
|
|
38
|
+
if (fs.existsSync(devPath)) {
|
|
39
|
+
this._juxscriptPath = devPath;
|
|
40
|
+
return devPath;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Priority 3: Sibling in monorepo
|
|
44
|
+
const monoPath = path.resolve(projectRoot, '../jux/index.js');
|
|
45
|
+
if (fs.existsSync(monoPath)) {
|
|
46
|
+
this._juxscriptPath = monoPath;
|
|
47
|
+
return monoPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get the components directory for CSS copying
|
|
55
|
+
*/
|
|
56
|
+
getComponentsDir() {
|
|
57
|
+
const juxPath = this.findJuxscriptPath();
|
|
58
|
+
if (!juxPath) return null;
|
|
59
|
+
return path.resolve(path.dirname(juxPath), 'lib/componentsv2');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
scanFiles() {
|
|
63
|
+
const files = fs.readdirSync(this.srcDir)
|
|
64
|
+
.filter(f => f.endsWith('.jux') || f.endsWith('.js'));
|
|
65
|
+
|
|
66
|
+
const views = [], dataModules = [], sharedModules = [];
|
|
67
|
+
|
|
68
|
+
files.forEach(file => {
|
|
69
|
+
const content = fs.readFileSync(path.join(this.srcDir, file), 'utf8');
|
|
70
|
+
const name = file.replace(/\.[^/.]+$/, '');
|
|
71
|
+
|
|
72
|
+
if (file.includes('data')) {
|
|
73
|
+
dataModules.push({ name, file, content });
|
|
74
|
+
} else if (/export\s+(function|const|let|var|class)\s+/.test(content)) {
|
|
75
|
+
sharedModules.push({ name, file, content });
|
|
76
|
+
} else {
|
|
77
|
+
views.push({ name, file, content });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return { views, dataModules, sharedModules };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
removeImports(code) {
|
|
85
|
+
return code
|
|
86
|
+
.replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
87
|
+
.replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
|
|
88
|
+
.replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
89
|
+
.replace(/^\s*import\s*;?\s*$/gm, '');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
sanitizeName(name) {
|
|
93
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
copyJuxscriptCss() {
|
|
97
|
+
const componentsDir = this.getComponentsDir();
|
|
98
|
+
if (!componentsDir) return [];
|
|
99
|
+
|
|
100
|
+
const cssFiles = [];
|
|
101
|
+
|
|
102
|
+
const walkDir = (dir, relativePath = '') => {
|
|
103
|
+
if (!fs.existsSync(dir)) return;
|
|
104
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
for (const entry of entries) {
|
|
106
|
+
const fullPath = path.join(dir, entry.name);
|
|
107
|
+
const relPath = path.join(relativePath, entry.name);
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
walkDir(fullPath, relPath);
|
|
110
|
+
} else if (entry.name === 'structure.css') {
|
|
111
|
+
cssFiles.push({ src: fullPath, dest: relPath, component: relativePath });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
walkDir(componentsDir);
|
|
117
|
+
|
|
118
|
+
const cssDistDir = path.join(this.distDir, 'css');
|
|
119
|
+
fs.mkdirSync(cssDistDir, { recursive: true });
|
|
120
|
+
|
|
121
|
+
cssFiles.forEach(css => {
|
|
122
|
+
const destPath = path.join(cssDistDir, css.dest);
|
|
123
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
124
|
+
fs.copyFileSync(css.src, destPath);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
console.log(`š Copied ${cssFiles.length} CSS files`);
|
|
128
|
+
return cssFiles;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
generateCssLinks(cssFiles) {
|
|
132
|
+
return cssFiles.map(css => ` <link rel="stylesheet" href="./css/${css.dest}">`).join('\n');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async loadJuxscriptExports() {
|
|
136
|
+
if (this._juxscriptExports) return this._juxscriptExports;
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
140
|
+
if (juxscriptPath) {
|
|
141
|
+
const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
|
|
142
|
+
const exports = new Set();
|
|
143
|
+
|
|
144
|
+
// Parse export statements
|
|
145
|
+
for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
|
|
146
|
+
match[1].split(',').forEach(exp => {
|
|
147
|
+
const name = exp.trim().split(/\s+as\s+/)[0].trim();
|
|
148
|
+
if (name) exports.add(name);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this._juxscriptExports = [...exports];
|
|
153
|
+
if (this._juxscriptExports.length > 0) {
|
|
154
|
+
console.log(`š¦ juxscript exports: ${this._juxscriptExports.join(', ')}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (err) {
|
|
158
|
+
this._juxscriptExports = [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this._juxscriptExports;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
validateViewCode(viewName, code) {
|
|
165
|
+
const issues = [];
|
|
166
|
+
|
|
167
|
+
let ast;
|
|
168
|
+
try {
|
|
169
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
170
|
+
} catch (parseError) {
|
|
171
|
+
issues.push({
|
|
172
|
+
type: 'error',
|
|
173
|
+
view: viewName,
|
|
174
|
+
line: parseError.loc?.line || 0,
|
|
175
|
+
message: `Syntax error: ${parseError.message}`,
|
|
176
|
+
code: ''
|
|
177
|
+
});
|
|
178
|
+
return issues;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const juxscriptImports = new Set();
|
|
182
|
+
const allImports = new Set();
|
|
183
|
+
|
|
184
|
+
walk(ast, {
|
|
185
|
+
ImportDeclaration(node) {
|
|
186
|
+
node.specifiers.forEach(spec => {
|
|
187
|
+
allImports.add(spec.local.name);
|
|
188
|
+
if (node.source.value === 'juxscript') {
|
|
189
|
+
juxscriptImports.add(spec.local.name);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const knownComponents = this._juxscriptExports || ['Element', 'Grid', 'Input', 'List'];
|
|
196
|
+
|
|
197
|
+
walk(ast, {
|
|
198
|
+
Identifier(node, parent) {
|
|
199
|
+
if (parent?.type === 'CallExpression' && parent.callee === node) {
|
|
200
|
+
const name = node.name;
|
|
201
|
+
if (/^[A-Z]/.test(name) && !allImports.has(name) && knownComponents.includes(name)) {
|
|
202
|
+
issues.push({
|
|
203
|
+
type: 'warning',
|
|
204
|
+
view: viewName,
|
|
205
|
+
line: node.loc?.start?.line || 0,
|
|
206
|
+
message: `"${name}" is used but not imported from juxscript`,
|
|
207
|
+
code: ''
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return issues;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
generateEntryPoint(views, dataModules, sharedModules) {
|
|
218
|
+
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
219
|
+
const allIssues = [];
|
|
220
|
+
const sourceSnapshot = {};
|
|
221
|
+
|
|
222
|
+
const juxImports = new Set();
|
|
223
|
+
[...views, ...dataModules, ...sharedModules].forEach(m => {
|
|
224
|
+
for (const match of m.content.matchAll(/import\s*\{\s*([^}]+)\s*\}\s*from\s*['"]juxscript['"]/g)) {
|
|
225
|
+
match[1].split(',').map(s => s.trim()).forEach(imp => {
|
|
226
|
+
if (imp) juxImports.add(imp);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (juxImports.size > 0) {
|
|
232
|
+
entry += `import { ${[...juxImports].sort().join(', ')} } from 'juxscript';\n\n`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
dataModules.forEach(m => {
|
|
236
|
+
entry += `import * as ${this.sanitizeName(m.name)}Data from './jux/${m.file}';\n`;
|
|
237
|
+
});
|
|
238
|
+
sharedModules.forEach(m => {
|
|
239
|
+
entry += `import * as ${this.sanitizeName(m.name)}Shared from './jux/${m.file}';\n`;
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
entry += `\n// Expose to window\n`;
|
|
243
|
+
dataModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Data);\n`);
|
|
244
|
+
sharedModules.forEach(m => entry += `Object.assign(window, ${this.sanitizeName(m.name)}Shared);\n`);
|
|
245
|
+
|
|
246
|
+
if (juxImports.size > 0) {
|
|
247
|
+
entry += `\nObject.assign(window, { ${[...juxImports].join(', ')} });\n`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
251
|
+
|
|
252
|
+
views.forEach(v => {
|
|
253
|
+
const capitalized = v.name.charAt(0).toUpperCase() + v.name.slice(1);
|
|
254
|
+
allIssues.push(...this.validateViewCode(v.name, v.content));
|
|
255
|
+
|
|
256
|
+
sourceSnapshot[v.file] = {
|
|
257
|
+
name: v.name,
|
|
258
|
+
file: v.file,
|
|
259
|
+
content: v.content,
|
|
260
|
+
lines: v.content.split('\n')
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
let viewCode = this.removeImports(v.content).replace(/^\s*export\s+default\s+.*$/gm, '');
|
|
264
|
+
const asyncPrefix = viewCode.includes('await ') ? 'async ' : '';
|
|
265
|
+
|
|
266
|
+
entry += `\n${asyncPrefix}function render${capitalized}() {\n${viewCode}\n}\n`;
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
dataModules.forEach(m => {
|
|
270
|
+
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
271
|
+
});
|
|
272
|
+
sharedModules.forEach(m => {
|
|
273
|
+
sourceSnapshot[m.file] = { name: m.name, file: m.file, content: m.content, lines: m.content.split('\n') };
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
this._sourceSnapshot = sourceSnapshot;
|
|
277
|
+
this._validationIssues = allIssues;
|
|
278
|
+
entry += this._generateRouter(views);
|
|
279
|
+
return entry;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
reportValidationIssues() {
|
|
283
|
+
const issues = this._validationIssues || [];
|
|
284
|
+
const errors = issues.filter(i => i.type === 'error');
|
|
285
|
+
const warnings = issues.filter(i => i.type === 'warning');
|
|
286
|
+
|
|
287
|
+
if (issues.length > 0) {
|
|
288
|
+
console.log('\nā ļø Validation Issues:\n');
|
|
289
|
+
issues.forEach(issue => {
|
|
290
|
+
const icon = issue.type === 'error' ? 'ā' : 'ā ļø';
|
|
291
|
+
console.log(`${icon} [${issue.view}:${issue.line}] ${issue.message}`);
|
|
292
|
+
});
|
|
293
|
+
console.log('');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return { isValid: errors.length === 0, errors, warnings };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_generateRouter(views) {
|
|
300
|
+
let routeMap = '';
|
|
301
|
+
views.forEach(v => {
|
|
302
|
+
const cap = v.name.charAt(0).toUpperCase() + v.name.slice(1);
|
|
303
|
+
if (v.name.toLowerCase() === 'index') routeMap += ` '/': render${cap},\n`;
|
|
304
|
+
routeMap += ` '/${v.name.toLowerCase()}': render${cap},\n`;
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
return `
|
|
308
|
+
// --- JUX SOURCE LOADER ---
|
|
309
|
+
var __juxSources = null;
|
|
310
|
+
async function __juxLoadSources() {
|
|
311
|
+
if (__juxSources) return __juxSources;
|
|
312
|
+
try {
|
|
313
|
+
var res = await fetch('/__jux_sources.json');
|
|
314
|
+
__juxSources = await res.json();
|
|
315
|
+
} catch (e) {
|
|
316
|
+
__juxSources = {};
|
|
317
|
+
}
|
|
318
|
+
return __juxSources;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Find source file from error stack
|
|
322
|
+
function __juxFindSource(stack) {
|
|
323
|
+
var match = stack.match(/render(\\w+)/);
|
|
324
|
+
if (match) {
|
|
325
|
+
var viewName = match[1].toLowerCase();
|
|
326
|
+
for (var file in __juxSources || {}) {
|
|
327
|
+
if (__juxSources[file].name.toLowerCase() === viewName) {
|
|
328
|
+
return { file: file, source: __juxSources[file], viewName: match[1] };
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- JUX RUNTIME ERROR OVERLAY ---
|
|
336
|
+
var __juxErrorOverlay = {
|
|
337
|
+
styles: \`
|
|
338
|
+
#__jux-error-overlay {
|
|
339
|
+
position: fixed; inset: 0; z-index: 99999;
|
|
340
|
+
background: rgba(0, 0, 0, 0.4);
|
|
341
|
+
display: flex; align-items: center; justify-content: center;
|
|
342
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, monospace;
|
|
343
|
+
opacity: 0;
|
|
344
|
+
transition: opacity 0.2s ease-out;
|
|
345
|
+
backdrop-filter: blur(2px);
|
|
346
|
+
}
|
|
347
|
+
#__jux-error-overlay.visible { opacity: 1; }
|
|
348
|
+
#__jux-error-overlay * { box-sizing: border-box; }
|
|
349
|
+
.__jux-modal {
|
|
350
|
+
background: #f8f9fa;
|
|
351
|
+
border-radius: 4px;
|
|
352
|
+
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.4);
|
|
353
|
+
max-width: 80vw;
|
|
354
|
+
width: 90%;
|
|
355
|
+
max-height: 90vh;
|
|
356
|
+
overflow: hidden;
|
|
357
|
+
display: flex;
|
|
358
|
+
flex-direction: column;
|
|
359
|
+
transform: translateY(10px);
|
|
360
|
+
transition: transform 0.2s ease-out;
|
|
361
|
+
}
|
|
362
|
+
#__jux-error-overlay.visible .__jux-modal { transform: translateY(0); }
|
|
363
|
+
.__jux-header {
|
|
364
|
+
background: #fff;
|
|
365
|
+
padding: 20px 24px;
|
|
366
|
+
border-bottom: 1px solid #e5e7eb;
|
|
367
|
+
}
|
|
368
|
+
.__jux-header h3 {
|
|
369
|
+
margin: 0 0 6px; font-weight: 600; font-size: 11px;
|
|
370
|
+
color: #9ca3af; text-transform: uppercase; letter-spacing: 0.5px;
|
|
371
|
+
}
|
|
372
|
+
.__jux-header h1 {
|
|
373
|
+
margin: 0 0 8px; font-size: 18px; font-weight: 600; color: #dc2626;
|
|
374
|
+
line-height: 1.3;
|
|
375
|
+
}
|
|
376
|
+
.__jux-header .file-info { color: #6b7280; font-size: 13px; }
|
|
377
|
+
.__jux-header .file-info strong { color: #dc2626; font-weight: 600; }
|
|
378
|
+
.__jux-source { padding: 16px 24px; overflow: auto; flex: 1; background: #f8f9fa; }
|
|
379
|
+
.__jux-code { background: #fff; border-radius: 8px; overflow: hidden; border: 1px solid #e5e7eb; }
|
|
380
|
+
.__jux-code-line { display: flex; font-size: 13px; line-height: 1.7; }
|
|
381
|
+
.__jux-code-line.error { background: #fef2f2; }
|
|
382
|
+
.__jux-code-line.error .__jux-line-code { color: #dc2626; font-weight: 500; }
|
|
383
|
+
.__jux-code-line.context { background: #fefce8; }
|
|
384
|
+
.__jux-line-num {
|
|
385
|
+
min-width: 44px; padding: 4px 12px; text-align: right;
|
|
386
|
+
color: #9ca3af; background: #f9fafb; user-select: none;
|
|
387
|
+
border-right: 1px solid #e5e7eb; font-size: 12px;
|
|
388
|
+
}
|
|
389
|
+
.__jux-code-line.error .__jux-line-num { background: #fef2f2; color: #dc2626; }
|
|
390
|
+
.__jux-line-code { flex: 1; padding: 4px 16px; color: #374151; white-space: pre; overflow-x: auto; }
|
|
391
|
+
.__jux-footer {
|
|
392
|
+
padding: 16px 24px; background: #fff; border-top: 1px solid #e5e7eb;
|
|
393
|
+
display: flex; justify-content: space-between; align-items: center;
|
|
394
|
+
}
|
|
395
|
+
.__jux-tip { color: #6b7280; font-size: 12px; }
|
|
396
|
+
.__jux-dismiss {
|
|
397
|
+
background: #f3f4f6; color: #374151; border: 1px solid #d1d5db;
|
|
398
|
+
padding: 8px 16px; border-radius: 6px; cursor: pointer;
|
|
399
|
+
font-size: 13px; font-weight: 500; transition: background 0.15s;
|
|
400
|
+
}
|
|
401
|
+
.__jux-dismiss:hover { background: #e5e7eb; }
|
|
402
|
+
.__jux-no-source { color: #6b7280; padding: 24px; text-align: center; }
|
|
403
|
+
.__jux-no-source pre {
|
|
404
|
+
background: #fff; padding: 16px; border-radius: 8px; margin-top: 16px;
|
|
405
|
+
font-size: 11px; color: #6b7280; overflow-x: auto; text-align: left;
|
|
406
|
+
border: 1px solid #e5e7eb;
|
|
407
|
+
}
|
|
408
|
+
\`,
|
|
409
|
+
|
|
410
|
+
show: async function(error, title) {
|
|
411
|
+
title = title || 'Runtime Error';
|
|
412
|
+
|
|
413
|
+
var existing = document.getElementById('__jux-error-overlay');
|
|
414
|
+
if (existing) existing.remove();
|
|
415
|
+
|
|
416
|
+
await __juxLoadSources();
|
|
417
|
+
|
|
418
|
+
var overlay = document.createElement('div');
|
|
419
|
+
overlay.id = '__jux-error-overlay';
|
|
420
|
+
|
|
421
|
+
var stack = error && error.stack ? error.stack : '';
|
|
422
|
+
var msg = error && error.message ? error.message : String(error);
|
|
423
|
+
|
|
424
|
+
var found = __juxFindSource(stack);
|
|
425
|
+
var sourceHtml = '';
|
|
426
|
+
var fileInfo = '';
|
|
427
|
+
|
|
428
|
+
if (found && found.source && found.source.lines) {
|
|
429
|
+
var lines = found.source.lines;
|
|
430
|
+
fileInfo = '<span class="file-info">in <strong>' + found.file + '</strong></span>';
|
|
431
|
+
|
|
432
|
+
var errorLineIndex = -1;
|
|
433
|
+
var errorMethod = msg.match(/\\.([a-zA-Z]+)\\s*is not a function/);
|
|
434
|
+
if (errorMethod) {
|
|
435
|
+
for (var i = 0; i < lines.length; i++) {
|
|
436
|
+
if (lines[i].indexOf('.' + errorMethod[1]) > -1) {
|
|
437
|
+
errorLineIndex = i;
|
|
438
|
+
break;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (errorLineIndex === -1) {
|
|
444
|
+
for (var i = 0; i < lines.length; i++) {
|
|
445
|
+
if (lines[i].indexOf('throw ') > -1 || lines[i].indexOf('throw(') > -1) {
|
|
446
|
+
errorLineIndex = i;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
var contextStart = Math.max(0, errorLineIndex - 3);
|
|
453
|
+
var contextEnd = Math.min(lines.length - 1, errorLineIndex + 5);
|
|
454
|
+
|
|
455
|
+
if (errorLineIndex === -1) {
|
|
456
|
+
contextStart = 0;
|
|
457
|
+
contextEnd = Math.min(14, lines.length - 1);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
for (var i = contextStart; i <= contextEnd; i++) {
|
|
461
|
+
var isError = (i === errorLineIndex);
|
|
462
|
+
var isContext = !isError && errorLineIndex > -1 && Math.abs(i - errorLineIndex) <= 2;
|
|
463
|
+
var lineClass = isError ? ' error' : (isContext ? ' context' : '');
|
|
464
|
+
var lineCode = lines[i].replace(/</g, '<').replace(/>/g, '>') || ' ';
|
|
465
|
+
sourceHtml += '<div class="__jux-code-line' + lineClass + '">' +
|
|
466
|
+
'<span class="__jux-line-num">' + (i + 1) + '</span>' +
|
|
467
|
+
'<span class="__jux-line-code">' + lineCode + '</span>' +
|
|
468
|
+
'</div>';
|
|
469
|
+
}
|
|
470
|
+
} else {
|
|
471
|
+
sourceHtml = '<div class="__jux-no-source">' +
|
|
472
|
+
'<p>Could not locate source file for this error.</p>' +
|
|
473
|
+
'<pre>' + stack + '</pre>' +
|
|
474
|
+
'</div>';
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
overlay.innerHTML = '<style>' + this.styles + '</style>' +
|
|
478
|
+
'<div class="__jux-modal">' +
|
|
479
|
+
'<div class="__jux-header">' +
|
|
480
|
+
'<h3>' + title + '</h3>' +
|
|
481
|
+
'<h1>' + msg + '</h1>' +
|
|
482
|
+
fileInfo +
|
|
483
|
+
'</div>' +
|
|
484
|
+
'<div class="__jux-source">' +
|
|
485
|
+
'<div class="__jux-code">' + sourceHtml + '</div>' +
|
|
486
|
+
'</div>' +
|
|
487
|
+
'<div class="__jux-footer">' +
|
|
488
|
+
'<span class="__jux-tip">š” Fix the error and save to reload</span>' +
|
|
489
|
+
'<button class="__jux-dismiss" onclick="document.getElementById(\\'__jux-error-overlay\\').remove()">Dismiss</button>' +
|
|
490
|
+
'</div>' +
|
|
491
|
+
'</div>';
|
|
492
|
+
|
|
493
|
+
document.body.appendChild(overlay);
|
|
494
|
+
|
|
495
|
+
// Trigger transition
|
|
496
|
+
requestAnimationFrame(function() {
|
|
497
|
+
overlay.classList.add('visible');
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
console.error(title + ':', error);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Global error handlers
|
|
505
|
+
window.addEventListener('error', function(e) {
|
|
506
|
+
__juxErrorOverlay.show(e.error || new Error(e.message), 'Uncaught Error');
|
|
507
|
+
}, true);
|
|
508
|
+
|
|
509
|
+
window.addEventListener('unhandledrejection', function(e) {
|
|
510
|
+
__juxErrorOverlay.show(e.reason || new Error('Promise rejected'), 'Unhandled Promise Rejection');
|
|
511
|
+
}, true);
|
|
512
|
+
|
|
513
|
+
// --- JUX ROUTER ---
|
|
514
|
+
const routes = {\n${routeMap}};
|
|
515
|
+
|
|
516
|
+
async function navigate(path) {
|
|
517
|
+
const view = routes[path];
|
|
518
|
+
if (!view) {
|
|
519
|
+
document.getElementById('app').innerHTML = '<h1 style="padding:40px;">404 - Not Found</h1>';
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
document.getElementById('app').innerHTML = '';
|
|
523
|
+
|
|
524
|
+
var overlay = document.getElementById('__jux-error-overlay');
|
|
525
|
+
if (overlay) overlay.remove();
|
|
526
|
+
|
|
527
|
+
try {
|
|
528
|
+
await view();
|
|
529
|
+
} catch (err) {
|
|
530
|
+
__juxErrorOverlay.show(err, 'Jux Render Error');
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
document.addEventListener('click', e => {
|
|
535
|
+
const a = e.target.closest('a');
|
|
536
|
+
if (!a || a.dataset.router === 'false') return;
|
|
537
|
+
try { if (new URL(a.href, location.origin).origin !== location.origin) return; } catch { return; }
|
|
538
|
+
e.preventDefault();
|
|
539
|
+
history.pushState({}, '', a.href);
|
|
540
|
+
navigate(new URL(a.href, location.origin).pathname);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
window.addEventListener('popstate', () => navigate(location.pathname));
|
|
544
|
+
navigate(location.pathname);
|
|
545
|
+
`;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
async build() {
|
|
549
|
+
console.log('š JUX Build\n');
|
|
550
|
+
|
|
551
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
552
|
+
if (!juxscriptPath) {
|
|
553
|
+
console.error('ā Could not locate juxscript package');
|
|
554
|
+
return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
|
|
555
|
+
}
|
|
556
|
+
console.log(`š¦ Using: ${juxscriptPath}`);
|
|
557
|
+
|
|
558
|
+
await this.loadJuxscriptExports();
|
|
559
|
+
|
|
560
|
+
if (fs.existsSync(this.distDir)) {
|
|
561
|
+
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
562
|
+
}
|
|
563
|
+
fs.mkdirSync(this.distDir, { recursive: true });
|
|
564
|
+
|
|
565
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
566
|
+
console.log(`š Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data\n`);
|
|
567
|
+
|
|
568
|
+
const cssFiles = this.copyJuxscriptCss();
|
|
569
|
+
|
|
570
|
+
// Copy data/shared modules to dist
|
|
571
|
+
const juxDistDir = path.join(this.distDir, 'jux');
|
|
572
|
+
fs.mkdirSync(juxDistDir, { recursive: true });
|
|
573
|
+
[...dataModules, ...sharedModules].forEach(m => {
|
|
574
|
+
fs.writeFileSync(path.join(juxDistDir, m.file), m.content);
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
578
|
+
const entryPath = path.join(this.distDir, 'entry.js');
|
|
579
|
+
fs.writeFileSync(entryPath, entryContent);
|
|
580
|
+
|
|
581
|
+
// Write source snapshot for dev tools
|
|
582
|
+
const snapshotPath = path.join(this.distDir, '__jux_sources.json');
|
|
583
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
584
|
+
console.log(`šø Source snapshot written`);
|
|
585
|
+
|
|
586
|
+
const validation = this.reportValidationIssues();
|
|
587
|
+
if (!validation.isValid) {
|
|
588
|
+
console.log('š BUILD FAILED\n');
|
|
589
|
+
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
try {
|
|
593
|
+
await esbuild.build({
|
|
594
|
+
entryPoints: [entryPath],
|
|
595
|
+
bundle: true,
|
|
596
|
+
outfile: path.join(this.distDir, 'bundle.js'),
|
|
597
|
+
format: 'esm',
|
|
598
|
+
platform: 'browser',
|
|
599
|
+
target: 'esnext',
|
|
600
|
+
sourcemap: true,
|
|
601
|
+
loader: { '.jux': 'js', '.css': 'empty' },
|
|
602
|
+
plugins: [{
|
|
603
|
+
name: 'juxscript-resolver',
|
|
604
|
+
setup: (build) => {
|
|
605
|
+
build.onResolve({ filter: /^juxscript$/ }, () => ({ path: juxscriptPath }));
|
|
606
|
+
}
|
|
607
|
+
}],
|
|
608
|
+
});
|
|
609
|
+
console.log('ā
esbuild complete');
|
|
610
|
+
} catch (err) {
|
|
611
|
+
console.error('ā esbuild failed:', err);
|
|
612
|
+
return { success: false, errors: [{ message: err.message }], warnings: [] };
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Generate index.html
|
|
616
|
+
const html = `<!DOCTYPE html>
|
|
617
|
+
<html lang="en">
|
|
618
|
+
<head>
|
|
619
|
+
<meta charset="UTF-8">
|
|
620
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
621
|
+
<title>JUX App</title>
|
|
622
|
+
${this.generateCssLinks(cssFiles)}
|
|
623
|
+
<script type="module" src="./bundle.js"></script>
|
|
624
|
+
</head>
|
|
625
|
+
<body>
|
|
626
|
+
<div id="app"></div>
|
|
627
|
+
</body>
|
|
628
|
+
</html>`;
|
|
629
|
+
fs.writeFileSync(path.join(this.distDir, 'index.html'), html);
|
|
630
|
+
|
|
631
|
+
// Cleanup temp files
|
|
632
|
+
fs.unlinkSync(entryPath);
|
|
633
|
+
fs.rmSync(juxDistDir, { recursive: true, force: true });
|
|
634
|
+
|
|
635
|
+
console.log(`\nā
Build Complete!\n`);
|
|
636
|
+
return { success: true, errors: [], warnings: validation.warnings };
|
|
637
|
+
}
|
|
638
|
+
}
|