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