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
|
@@ -0,0 +1,834 @@
|
|
|
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
|
+
|
|
63
|
+
const scanDirectory = (currentDir) => {
|
|
64
|
+
const entries = fs.readdirSync(currentDir, { withFileTypes: true });
|
|
65
|
+
|
|
66
|
+
for (const entry of entries) {
|
|
67
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
68
|
+
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
scanDirectory(fullPath);
|
|
71
|
+
} else if (entry.isFile()) {
|
|
72
|
+
const file = entry.name;
|
|
73
|
+
|
|
74
|
+
if ((file.endsWith('.jux') || file.endsWith('.js')) && !this.isAssetFile(file)) {
|
|
75
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
76
|
+
const relativePath = path.relative(this.srcDir, fullPath);
|
|
77
|
+
const name = relativePath.replace(/\.[^/.]+$/, '');
|
|
78
|
+
|
|
79
|
+
// Check if it has exports (module) or is executable code (view)
|
|
80
|
+
const hasExports = /export\s+(default|const|function|class|{)/.test(content);
|
|
81
|
+
|
|
82
|
+
if (file.includes('data')) {
|
|
83
|
+
dataModules.push({
|
|
84
|
+
name,
|
|
85
|
+
file: relativePath,
|
|
86
|
+
content,
|
|
87
|
+
originalContent: content
|
|
88
|
+
});
|
|
89
|
+
} else if (hasExports) {
|
|
90
|
+
// ✅ Any file with exports is a module (not just .js files)
|
|
91
|
+
sharedModules.push({
|
|
92
|
+
name,
|
|
93
|
+
file: relativePath,
|
|
94
|
+
content,
|
|
95
|
+
originalContent: content
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// .jux files without exports are views - use AST to extract imports
|
|
99
|
+
let wrappedContent;
|
|
100
|
+
try {
|
|
101
|
+
const ast = acorn.parse(content, {
|
|
102
|
+
ecmaVersion: 'latest',
|
|
103
|
+
sourceType: 'module',
|
|
104
|
+
locations: true
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const imports = [];
|
|
108
|
+
let lastImportEnd = 0;
|
|
109
|
+
|
|
110
|
+
// Collect imports and track where they end
|
|
111
|
+
for (const node of ast.body) {
|
|
112
|
+
if (node.type === 'ImportDeclaration') {
|
|
113
|
+
imports.push(content.substring(node.start, node.end));
|
|
114
|
+
lastImportEnd = node.end;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Get the rest of the code (everything after imports)
|
|
119
|
+
const restOfCode = content.substring(lastImportEnd).trim();
|
|
120
|
+
|
|
121
|
+
// Build wrapped content
|
|
122
|
+
wrappedContent = [
|
|
123
|
+
...imports,
|
|
124
|
+
'',
|
|
125
|
+
'export default async function() {',
|
|
126
|
+
restOfCode,
|
|
127
|
+
'}'
|
|
128
|
+
].join('\n');
|
|
129
|
+
|
|
130
|
+
} catch (parseError) {
|
|
131
|
+
// Fallback: if parsing fails, just wrap the whole thing
|
|
132
|
+
console.warn(`⚠️ Could not parse ${relativePath}, using basic wrapping`);
|
|
133
|
+
wrappedContent = `export default async function() {\n${content}\n}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
views.push({
|
|
137
|
+
name,
|
|
138
|
+
file: relativePath,
|
|
139
|
+
content: wrappedContent,
|
|
140
|
+
originalContent: content
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
scanDirectory(this.srcDir);
|
|
149
|
+
return { views, dataModules, sharedModules };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isAssetFile(filename) {
|
|
153
|
+
const assetExtensions = ['.css', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.woff', '.woff2', '.ttf', '.eot'];
|
|
154
|
+
return assetExtensions.some(ext => filename.endsWith(ext));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
removeImports(code) {
|
|
158
|
+
return code
|
|
159
|
+
.replace(/^\s*import\s+.*?from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
160
|
+
.replace(/^\s*import\s*\{[\s\S]*?\}\s*from\s*['"][^'"]+['"][\s;]*/gm, '')
|
|
161
|
+
.replace(/^\s*import\s+\w+\s+from\s+['"][^'"]+['"][\s;]*$/gm, '')
|
|
162
|
+
.replace(/^\s*import\s*;?\s*$/gm, '');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
sanitizeName(name) {
|
|
166
|
+
return name.replace(/[^a-zA-Z0-9]/g, '_');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async loadJuxscriptExports() {
|
|
170
|
+
if (this._juxscriptExports) return this._juxscriptExports;
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
174
|
+
if (juxscriptPath) {
|
|
175
|
+
const indexContent = fs.readFileSync(juxscriptPath, 'utf8');
|
|
176
|
+
const exports = new Set();
|
|
177
|
+
|
|
178
|
+
for (const match of indexContent.matchAll(/export\s*\{\s*([^}]+)\s*\}/g)) {
|
|
179
|
+
match[1].split(',').forEach(exp => {
|
|
180
|
+
const name = exp.trim().split(/\s+as\s+/)[0].trim();
|
|
181
|
+
if (name) exports.add(name);
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
this._juxscriptExports = [...exports];
|
|
186
|
+
if (this._juxscriptExports.length > 0) {
|
|
187
|
+
console.log(`📦 juxscript exports: ${this._juxscriptExports.join(', ')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
} catch (err) {
|
|
191
|
+
this._juxscriptExports = [];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this._juxscriptExports;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
validateViewCode(viewName, code) {
|
|
198
|
+
const issues = [];
|
|
199
|
+
|
|
200
|
+
let ast;
|
|
201
|
+
try {
|
|
202
|
+
ast = acorn.parse(code, { ecmaVersion: 'latest', sourceType: 'module', locations: true });
|
|
203
|
+
} catch (parseError) {
|
|
204
|
+
issues.push({
|
|
205
|
+
type: 'error',
|
|
206
|
+
view: viewName,
|
|
207
|
+
line: parseError.loc?.line || 0,
|
|
208
|
+
message: `Syntax error: ${parseError.message}`,
|
|
209
|
+
code: ''
|
|
210
|
+
});
|
|
211
|
+
return issues;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const allImports = new Set();
|
|
215
|
+
|
|
216
|
+
walk(ast, {
|
|
217
|
+
ImportDeclaration(node) {
|
|
218
|
+
node.specifiers.forEach(spec => {
|
|
219
|
+
allImports.add(spec.local.name);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Default known facades/components if load fails
|
|
225
|
+
const knownComponents = this._juxscriptExports || ['element', 'input', 'buttonGroup'];
|
|
226
|
+
|
|
227
|
+
walk(ast, {
|
|
228
|
+
Identifier(node, parent) {
|
|
229
|
+
if (parent?.type === 'CallExpression' && parent.callee === node) {
|
|
230
|
+
const name = node.name;
|
|
231
|
+
if (!allImports.has(name) && knownComponents.includes(name)) {
|
|
232
|
+
issues.push({
|
|
233
|
+
type: 'warning',
|
|
234
|
+
view: viewName,
|
|
235
|
+
line: node.loc?.start?.line || 0,
|
|
236
|
+
message: `"${name}" is used but not imported from juxscript`,
|
|
237
|
+
code: ''
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
return issues;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Generate entry point - inline EVERYTHING
|
|
249
|
+
*/
|
|
250
|
+
generateEntryPoint(views, dataModules, sharedModules) {
|
|
251
|
+
let entry = `// Auto-generated JUX entry point\n\n`;
|
|
252
|
+
const sourceSnapshot = {};
|
|
253
|
+
|
|
254
|
+
// ✅ Collect ALL unique imports from ALL files (views + shared + data)
|
|
255
|
+
const allImports = new Map(); // Map<resolvedPath, Set<importedNames>>
|
|
256
|
+
const allFiles = [...views, ...sharedModules, ...dataModules];
|
|
257
|
+
|
|
258
|
+
allFiles.forEach(file => {
|
|
259
|
+
const codeBody = file.originalContent || file.content;
|
|
260
|
+
const filePath = path.join(this.srcDir, file.file);
|
|
261
|
+
|
|
262
|
+
try {
|
|
263
|
+
const ast = acorn.parse(codeBody, {
|
|
264
|
+
ecmaVersion: 'latest',
|
|
265
|
+
sourceType: 'module',
|
|
266
|
+
locations: true
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Extract import statements
|
|
270
|
+
ast.body.filter(node => node.type === 'ImportDeclaration').forEach(node => {
|
|
271
|
+
const importPath = node.source.value;
|
|
272
|
+
|
|
273
|
+
// ✅ Resolve the import path
|
|
274
|
+
let resolvedImportPath;
|
|
275
|
+
if (importPath.startsWith('.')) {
|
|
276
|
+
// Relative import
|
|
277
|
+
const absolutePath = path.resolve(path.dirname(filePath), importPath);
|
|
278
|
+
resolvedImportPath = path.relative(this.srcDir, absolutePath);
|
|
279
|
+
resolvedImportPath = resolvedImportPath.replace(/\\/g, '/');
|
|
280
|
+
} else {
|
|
281
|
+
// Module import like 'juxscript', 'axios'
|
|
282
|
+
resolvedImportPath = importPath;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Initialize Set for this path
|
|
286
|
+
if (!allImports.has(resolvedImportPath)) {
|
|
287
|
+
allImports.set(resolvedImportPath, new Set());
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Add imported names
|
|
291
|
+
node.specifiers.forEach(spec => {
|
|
292
|
+
if (spec.type === 'ImportSpecifier') {
|
|
293
|
+
allImports.get(resolvedImportPath).add(spec.imported.name);
|
|
294
|
+
} else if (spec.type === 'ImportDefaultSpecifier') {
|
|
295
|
+
allImports.get(resolvedImportPath).add('default:' + spec.local.name);
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
} catch (parseError) {
|
|
300
|
+
console.warn(`⚠️ Could not parse ${file.file} for imports`);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ✅ Filter out imports that point to our own files (these will be inlined)
|
|
305
|
+
const externalImports = new Map();
|
|
306
|
+
allImports.forEach((names, resolvedPath) => {
|
|
307
|
+
// Only include if it's NOT a file in our src directory
|
|
308
|
+
if (!resolvedPath.endsWith('.js') && !resolvedPath.endsWith('.jux')) {
|
|
309
|
+
externalImports.set(resolvedPath, names);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ✅ Write external imports only (juxscript, axios, etc.)
|
|
314
|
+
externalImports.forEach((names, importPath) => {
|
|
315
|
+
const namedImports = Array.from(names).filter(n => !n.startsWith('default:'));
|
|
316
|
+
const defaultImports = Array.from(names).filter(n => n.startsWith('default:')).map(n => n.split(':')[1]);
|
|
317
|
+
|
|
318
|
+
if (defaultImports.length > 0) {
|
|
319
|
+
defaultImports.forEach(name => {
|
|
320
|
+
entry += `import ${name} from '${importPath}';\n`;
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (namedImports.length > 0) {
|
|
325
|
+
entry += `import { ${namedImports.join(', ')} } from '${importPath}';\n`;
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
entry += '\n';
|
|
330
|
+
|
|
331
|
+
// ✅ Inline shared modules as constants/functions
|
|
332
|
+
entry += `// --- SHARED MODULES (INLINED) ---\n`;
|
|
333
|
+
sharedModules.forEach(m => {
|
|
334
|
+
sourceSnapshot[m.file] = {
|
|
335
|
+
name: m.name,
|
|
336
|
+
file: m.file,
|
|
337
|
+
content: m.content,
|
|
338
|
+
lines: m.content.split('\n')
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
// Remove imports and exports, just keep the code
|
|
342
|
+
let code = m.originalContent || m.content;
|
|
343
|
+
code = this._stripImportsAndExports(code);
|
|
344
|
+
|
|
345
|
+
entry += `\n// From: ${m.file}\n${code}\n`;
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ✅ Inline data modules
|
|
349
|
+
entry += `\n// --- DATA MODULES (INLINED) ---\n`;
|
|
350
|
+
dataModules.forEach(m => {
|
|
351
|
+
sourceSnapshot[m.file] = {
|
|
352
|
+
name: m.name,
|
|
353
|
+
file: m.file,
|
|
354
|
+
content: m.content,
|
|
355
|
+
lines: m.content.split('\n')
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
let code = m.originalContent || m.content;
|
|
359
|
+
code = this._stripImportsAndExports(code);
|
|
360
|
+
|
|
361
|
+
entry += `\n// From: ${m.file}\n${code}\n`;
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// ✅ Expose everything to window (since we removed exports)
|
|
365
|
+
//entry += `\n// Expose to window\n`;
|
|
366
|
+
//entry += `Object.assign(window, { jux, state, registry, stateHistory, Link, link, navLink, externalLink, layer, overlay, VStack, HStack, ZStack, vstack, hstack, zstack });\n`;
|
|
367
|
+
|
|
368
|
+
entry += `\n// --- VIEW FUNCTIONS ---\n`;
|
|
369
|
+
|
|
370
|
+
const routeToFunctionMap = new Map();
|
|
371
|
+
|
|
372
|
+
// ✅ Process views: strip imports, inline as functions
|
|
373
|
+
views.forEach((v, index) => {
|
|
374
|
+
const functionName = `renderJux${index}`;
|
|
375
|
+
|
|
376
|
+
sourceSnapshot[v.file] = {
|
|
377
|
+
name: v.name,
|
|
378
|
+
file: v.file,
|
|
379
|
+
content: v.originalContent || v.content,
|
|
380
|
+
lines: (v.originalContent || v.content).split('\n'),
|
|
381
|
+
functionName
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
let codeBody = v.originalContent || v.content;
|
|
385
|
+
codeBody = this._stripImportsAndExports(codeBody);
|
|
386
|
+
|
|
387
|
+
entry += `\nasync function ${functionName}() {\n${codeBody}\n}\n`;
|
|
388
|
+
|
|
389
|
+
// Generate route path
|
|
390
|
+
const routePath = v.name
|
|
391
|
+
.toLowerCase()
|
|
392
|
+
.replace(/\\/g, '/')
|
|
393
|
+
.replace(/\.jux$/i, '')
|
|
394
|
+
.replace(/\./g, '-')
|
|
395
|
+
.replace(/\s+/g, '-')
|
|
396
|
+
.replace(/[^a-z0-9\/_-]/g, '')
|
|
397
|
+
.replace(/-+/g, '-')
|
|
398
|
+
.replace(/^-|-$/g, '');
|
|
399
|
+
|
|
400
|
+
if (routePath === 'index' || routePath === '') {
|
|
401
|
+
routeToFunctionMap.set('/', functionName);
|
|
402
|
+
}
|
|
403
|
+
routeToFunctionMap.set(`/${routePath}`, functionName);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ✅ Generate router
|
|
407
|
+
entry += this._generateRouter(routeToFunctionMap);
|
|
408
|
+
|
|
409
|
+
this._sourceSnapshot = sourceSnapshot;
|
|
410
|
+
this._validationIssues = [];
|
|
411
|
+
|
|
412
|
+
return entry;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Strip imports and exports from code, keeping the actual logic
|
|
417
|
+
*/
|
|
418
|
+
_stripImportsAndExports(code) {
|
|
419
|
+
try {
|
|
420
|
+
const ast = acorn.parse(code, {
|
|
421
|
+
ecmaVersion: 'latest',
|
|
422
|
+
sourceType: 'module',
|
|
423
|
+
locations: true
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Remove imports and exports
|
|
427
|
+
const nodesToRemove = ast.body.filter(node =>
|
|
428
|
+
node.type === 'ImportDeclaration' ||
|
|
429
|
+
node.type === 'ExportNamedDeclaration' ||
|
|
430
|
+
node.type === 'ExportDefaultDeclaration' ||
|
|
431
|
+
node.type === 'ExportAllDeclaration'
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
if (nodesToRemove.length === 0) {
|
|
435
|
+
return code.trim();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Remove nodes in reverse order
|
|
439
|
+
let result = code;
|
|
440
|
+
let offset = 0;
|
|
441
|
+
|
|
442
|
+
nodesToRemove.forEach(node => {
|
|
443
|
+
let start = node.start - offset;
|
|
444
|
+
let end = node.end - offset;
|
|
445
|
+
|
|
446
|
+
// If it's an export with a declaration, keep the declaration
|
|
447
|
+
if (node.type === 'ExportNamedDeclaration' && node.declaration) {
|
|
448
|
+
// Keep the declaration part, remove just "export"
|
|
449
|
+
const declarationStart = node.declaration.start - offset;
|
|
450
|
+
result = result.substring(0, start) + result.substring(declarationStart);
|
|
451
|
+
offset += (declarationStart - start);
|
|
452
|
+
} else if (node.type === 'ExportDefaultDeclaration') {
|
|
453
|
+
// Remove "export default", keep the expression
|
|
454
|
+
const exportKeyword = result.substring(start, end).match(/export\s+default\s+/);
|
|
455
|
+
if (exportKeyword) {
|
|
456
|
+
result = result.substring(0, start) + result.substring(start + exportKeyword[0].length);
|
|
457
|
+
offset += exportKeyword[0].length;
|
|
458
|
+
}
|
|
459
|
+
} else {
|
|
460
|
+
// Remove the entire node
|
|
461
|
+
result = result.substring(0, start) + result.substring(end);
|
|
462
|
+
offset += (end - start);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
return result.trim();
|
|
467
|
+
} catch (parseError) {
|
|
468
|
+
console.warn(`⚠️ Could not parse code for stripping, keeping as-is`);
|
|
469
|
+
return code.trim();
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
async build() {
|
|
474
|
+
console.log('🚀 JUX Build\n');
|
|
475
|
+
|
|
476
|
+
const juxscriptPath = this.findJuxscriptPath();
|
|
477
|
+
if (!juxscriptPath) {
|
|
478
|
+
console.error('❌ Could not locate juxscript package');
|
|
479
|
+
return { success: false, errors: [{ message: 'juxscript not found' }], warnings: [] };
|
|
480
|
+
}
|
|
481
|
+
console.log(`📦 Using: ${juxscriptPath}`);
|
|
482
|
+
|
|
483
|
+
await this.loadJuxscriptExports();
|
|
484
|
+
|
|
485
|
+
if (fs.existsSync(this.distDir)) {
|
|
486
|
+
fs.rmSync(this.distDir, { recursive: true, force: true });
|
|
487
|
+
}
|
|
488
|
+
fs.mkdirSync(this.distDir, { recursive: true });
|
|
489
|
+
|
|
490
|
+
// ✅ Copy public folder if exists
|
|
491
|
+
this.copyPublicFolder();
|
|
492
|
+
|
|
493
|
+
const { views, dataModules, sharedModules } = this.scanFiles();
|
|
494
|
+
console.log(`📁 Found ${views.length} views, ${sharedModules.length} shared, ${dataModules.length} data`);
|
|
495
|
+
|
|
496
|
+
// ❌ REMOVE: No need to copy files to dist - everything goes in bundle
|
|
497
|
+
// const juxDistDir = path.join(this.distDir, 'jux');
|
|
498
|
+
// fs.mkdirSync(juxDistDir, { recursive: true });
|
|
499
|
+
// ... copying logic removed ...
|
|
500
|
+
|
|
501
|
+
const entryContent = this.generateEntryPoint(views, dataModules, sharedModules);
|
|
502
|
+
|
|
503
|
+
const entryPath = path.join(this.distDir, 'entry.js');
|
|
504
|
+
fs.writeFileSync(entryPath, entryContent);
|
|
505
|
+
|
|
506
|
+
const snapshotPath = path.join(this.distDir, '__jux_sources.json');
|
|
507
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(this._sourceSnapshot, null, 2));
|
|
508
|
+
console.log(`📸 Source snapshot written`);
|
|
509
|
+
|
|
510
|
+
const validation = this.reportValidationIssues();
|
|
511
|
+
if (!validation.isValid) {
|
|
512
|
+
console.log('🛑 BUILD FAILED\n');
|
|
513
|
+
return { success: false, errors: validation.errors, warnings: validation.warnings };
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
try {
|
|
517
|
+
console.log('🔧 Starting esbuild...\n');
|
|
518
|
+
|
|
519
|
+
await esbuild.build({
|
|
520
|
+
entryPoints: [entryPath],
|
|
521
|
+
bundle: true,
|
|
522
|
+
outfile: path.join(this.distDir, 'bundle.js'),
|
|
523
|
+
format: 'esm',
|
|
524
|
+
platform: 'browser',
|
|
525
|
+
target: 'esnext',
|
|
526
|
+
sourcemap: true,
|
|
527
|
+
|
|
528
|
+
loader: {
|
|
529
|
+
'.jux': 'js',
|
|
530
|
+
'.css': 'empty'
|
|
531
|
+
},
|
|
532
|
+
|
|
533
|
+
plugins: [{
|
|
534
|
+
name: 'juxscript-resolver',
|
|
535
|
+
setup: (build) => {
|
|
536
|
+
// Resolve juxscript
|
|
537
|
+
build.onResolve({ filter: /^juxscript$/ }, () => ({
|
|
538
|
+
path: juxscriptPath
|
|
539
|
+
}));
|
|
540
|
+
|
|
541
|
+
// ✅ Force axios to resolve from project's node_modules
|
|
542
|
+
build.onResolve({ filter: /^axios$/ }, () => {
|
|
543
|
+
const projectRoot = process.cwd();
|
|
544
|
+
const axiosPath = path.resolve(projectRoot, 'node_modules/axios/dist/esm/axios.js');
|
|
545
|
+
|
|
546
|
+
if (fs.existsSync(axiosPath)) {
|
|
547
|
+
console.log('✅ Found axios at:', axiosPath);
|
|
548
|
+
return { path: axiosPath };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.error('❌ axios not found in project node_modules');
|
|
552
|
+
return null;
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
// ✅ Resolve .jux file imports - with detailed logging
|
|
556
|
+
build.onResolve({ filter: /\.jux$/ }, (args) => {
|
|
557
|
+
console.log(`🔍 Resolving: ${args.path} from ${args.importer}`);
|
|
558
|
+
|
|
559
|
+
// Skip if already resolved
|
|
560
|
+
if (path.isAbsolute(args.path)) {
|
|
561
|
+
return { path: args.path };
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Handle relative imports
|
|
565
|
+
if (args.path.startsWith('.')) {
|
|
566
|
+
const importer = args.importer || entryPath;
|
|
567
|
+
const importerDir = path.dirname(importer);
|
|
568
|
+
const resolvedPath = path.resolve(importerDir, args.path);
|
|
569
|
+
|
|
570
|
+
console.log(` Importer dir: ${importerDir}`);
|
|
571
|
+
console.log(` Resolved to: ${resolvedPath}`);
|
|
572
|
+
console.log(` Exists: ${fs.existsSync(resolvedPath)}`);
|
|
573
|
+
|
|
574
|
+
if (fs.existsSync(resolvedPath)) {
|
|
575
|
+
return { path: resolvedPath };
|
|
576
|
+
} else {
|
|
577
|
+
console.error(`❌ Could not resolve ${args.path} from ${importer}`);
|
|
578
|
+
console.error(` Tried: ${resolvedPath}`);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return null;
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}],
|
|
586
|
+
|
|
587
|
+
mainFields: ['browser', 'module', 'main'],
|
|
588
|
+
conditions: ['browser', 'import', 'module', 'default'],
|
|
589
|
+
|
|
590
|
+
define: {
|
|
591
|
+
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'),
|
|
592
|
+
'global': 'globalThis',
|
|
593
|
+
'process.env': JSON.stringify({})
|
|
594
|
+
},
|
|
595
|
+
|
|
596
|
+
minify: false,
|
|
597
|
+
treeShaking: true,
|
|
598
|
+
metafile: true,
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
console.log('\n✅ Build complete');
|
|
602
|
+
|
|
603
|
+
const bundlePath = path.join(this.distDir, 'bundle.js');
|
|
604
|
+
const bundleStats = fs.statSync(bundlePath);
|
|
605
|
+
const bundleSizeKB = (bundleStats.size / 1024).toFixed(2);
|
|
606
|
+
console.log(`📦 Bundle size: ${bundleSizeKB} KB`);
|
|
607
|
+
|
|
608
|
+
} catch (err) {
|
|
609
|
+
console.error('❌ esbuild failed:', err);
|
|
610
|
+
return { success: false, errors: [{ message: err.message }], warnings: [] };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const html = `<!DOCTYPE html>
|
|
614
|
+
<html lang="en">
|
|
615
|
+
<head>
|
|
616
|
+
<meta charset="UTF-8">
|
|
617
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
618
|
+
<title>JUX Application</title>
|
|
619
|
+
<script type="module" src="/bundle.js"></script>
|
|
620
|
+
</head>
|
|
621
|
+
<body>
|
|
622
|
+
<div id="app"></div>
|
|
623
|
+
</body>
|
|
624
|
+
</html>`;
|
|
625
|
+
|
|
626
|
+
fs.writeFileSync(
|
|
627
|
+
path.join(this.config.distDir, 'index.html'),
|
|
628
|
+
html,
|
|
629
|
+
'utf8'
|
|
630
|
+
);
|
|
631
|
+
|
|
632
|
+
console.log('✅ Generated index.html\n');
|
|
633
|
+
return { success: true, errors: [], warnings: validation.warnings };
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Generate router code
|
|
638
|
+
*/
|
|
639
|
+
_generateRouter(routeToFunctionMap) {
|
|
640
|
+
let router = `\n// --- ROUTER ---\n`;
|
|
641
|
+
router += `const routes = {\n`;
|
|
642
|
+
|
|
643
|
+
routeToFunctionMap.forEach((functionName, route) => {
|
|
644
|
+
router += ` '${route}': ${functionName},\n`;
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
router += `};\n\n`;
|
|
648
|
+
|
|
649
|
+
router += `function route(path) {
|
|
650
|
+
const renderFn = routes[path] || routes['/'];
|
|
651
|
+
if (renderFn) {
|
|
652
|
+
// Clear main content area
|
|
653
|
+
const appMain = document.getElementById('appmain-content');
|
|
654
|
+
if (appMain) appMain.innerHTML = '';
|
|
655
|
+
|
|
656
|
+
const app = document.getElementById('app');
|
|
657
|
+
if (app) app.innerHTML = '';
|
|
658
|
+
|
|
659
|
+
// Call render function
|
|
660
|
+
if (typeof renderFn === 'function') {
|
|
661
|
+
renderFn();
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
const app = document.getElementById('app');
|
|
665
|
+
if (app) app.innerHTML = '<h1>404 - Page Not Found</h1>';
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Initial route
|
|
670
|
+
window.addEventListener('DOMContentLoaded', () => {
|
|
671
|
+
route(window.location.pathname);
|
|
672
|
+
});
|
|
673
|
+
|
|
674
|
+
// Handle navigation
|
|
675
|
+
window.addEventListener('popstate', () => route(window.location.pathname));
|
|
676
|
+
|
|
677
|
+
// Intercept link clicks for SPA navigation
|
|
678
|
+
document.addEventListener('click', (e) => {
|
|
679
|
+
const link = e.target.closest('a[href]');
|
|
680
|
+
if (link) {
|
|
681
|
+
const href = link.getAttribute('href');
|
|
682
|
+
if (href.startsWith('/') && !href.startsWith('//')) {
|
|
683
|
+
e.preventDefault();
|
|
684
|
+
window.history.pushState({}, '', href);
|
|
685
|
+
route(href);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
// Expose router for manual navigation
|
|
691
|
+
window.navigateTo = (path) => {
|
|
692
|
+
window.history.pushState({}, '', path);
|
|
693
|
+
route(path);
|
|
694
|
+
};
|
|
695
|
+
`;
|
|
696
|
+
|
|
697
|
+
return router;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Report validation issues
|
|
702
|
+
*/
|
|
703
|
+
reportValidationIssues() {
|
|
704
|
+
const errors = [];
|
|
705
|
+
const warnings = [];
|
|
706
|
+
|
|
707
|
+
// Check if there are any validation issues collected
|
|
708
|
+
if (this._validationIssues && this._validationIssues.length > 0) {
|
|
709
|
+
this._validationIssues.forEach(issue => {
|
|
710
|
+
if (issue.type === 'error') {
|
|
711
|
+
errors.push(issue);
|
|
712
|
+
} else if (issue.type === 'warning') {
|
|
713
|
+
warnings.push(issue);
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Log warnings
|
|
719
|
+
if (warnings.length > 0) {
|
|
720
|
+
console.log('\n⚠️ Warnings:\n');
|
|
721
|
+
warnings.forEach(w => {
|
|
722
|
+
console.log(` ${w.view}:${w.line} - ${w.message}`);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Log errors
|
|
727
|
+
if (errors.length > 0) {
|
|
728
|
+
console.log('\n❌ Errors:\n');
|
|
729
|
+
errors.forEach(e => {
|
|
730
|
+
console.log(` ${e.view}:${e.line} - ${e.message}`);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
isValid: errors.length === 0,
|
|
736
|
+
errors,
|
|
737
|
+
warnings
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Copy public folder contents to dist
|
|
743
|
+
*/
|
|
744
|
+
copyPublicFolder() {
|
|
745
|
+
// ✅ Use configured public path or resolve from paths object
|
|
746
|
+
const publicSrc = this.paths.public
|
|
747
|
+
? this.paths.public
|
|
748
|
+
: path.resolve(process.cwd(), this.publicDir);
|
|
749
|
+
|
|
750
|
+
if (!fs.existsSync(publicSrc)) {
|
|
751
|
+
return; // No public folder, skip
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
console.log('📦 Copying public assets...');
|
|
755
|
+
|
|
756
|
+
try {
|
|
757
|
+
this._copyDirRecursive(publicSrc, this.distDir, 0);
|
|
758
|
+
console.log('✅ Public assets copied');
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.warn('⚠️ Error copying public folder:', err.message);
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Recursively copy directory contents
|
|
766
|
+
*/
|
|
767
|
+
_copyDirRecursive(src, dest, depth = 0) {
|
|
768
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
769
|
+
|
|
770
|
+
entries.forEach(entry => {
|
|
771
|
+
// Skip hidden files and directories
|
|
772
|
+
if (entry.name.startsWith('.')) return;
|
|
773
|
+
|
|
774
|
+
const srcPath = path.join(src, entry.name);
|
|
775
|
+
const destPath = path.join(dest, entry.name);
|
|
776
|
+
|
|
777
|
+
if (entry.isDirectory()) {
|
|
778
|
+
// Create directory and recurse
|
|
779
|
+
if (!fs.existsSync(destPath)) {
|
|
780
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
781
|
+
}
|
|
782
|
+
this._copyDirRecursive(srcPath, destPath, depth + 1);
|
|
783
|
+
} else {
|
|
784
|
+
// Copy file
|
|
785
|
+
fs.copyFileSync(srcPath, destPath);
|
|
786
|
+
|
|
787
|
+
// Log files at root level only
|
|
788
|
+
if (depth === 0) {
|
|
789
|
+
const ext = path.extname(entry.name);
|
|
790
|
+
const icon = this._getFileIcon(ext);
|
|
791
|
+
console.log(` ${icon} ${entry.name}`);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* Get icon for file type
|
|
799
|
+
*/
|
|
800
|
+
_getFileIcon(ext) {
|
|
801
|
+
const icons = {
|
|
802
|
+
'.html': '📄',
|
|
803
|
+
'.css': '🎨',
|
|
804
|
+
'.js': '📜',
|
|
805
|
+
'.json': '📋',
|
|
806
|
+
'.png': '🖼️',
|
|
807
|
+
'.jpg': '🖼️',
|
|
808
|
+
'.jpeg': '🖼️',
|
|
809
|
+
'.gif': '🖼️',
|
|
810
|
+
'.svg': '🎨',
|
|
811
|
+
'.ico': '🔖',
|
|
812
|
+
'.woff': '🔤',
|
|
813
|
+
'.woff2': '🔤',
|
|
814
|
+
'.ttf': '🔤',
|
|
815
|
+
'.eot': '🔤'
|
|
816
|
+
};
|
|
817
|
+
return icons[ext.toLowerCase()] || '📦';
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
/**
|
|
821
|
+
* ✅ Generate valid JavaScript identifier from file path
|
|
822
|
+
* Example: abc/aaa.jux -> abc_aaa
|
|
823
|
+
* Example: menus/main.jux -> menus_main
|
|
824
|
+
* Example: pages/blog/post.jux -> pages_blog_post
|
|
825
|
+
*/
|
|
826
|
+
_generateNameFromPath(filepath) {
|
|
827
|
+
return filepath
|
|
828
|
+
.replace(/\.jux$/, '') // Remove .jux extension
|
|
829
|
+
.replace(/[\/\\]/g, '_') // Replace / and \ with _
|
|
830
|
+
.replace(/[^a-zA-Z0-9_]/g, '_') // Replace any other invalid chars with _
|
|
831
|
+
.replace(/_+/g, '_') // Collapse multiple consecutive underscores
|
|
832
|
+
.replace(/^_|_$/g, ''); // Remove leading/trailing underscores
|
|
833
|
+
}
|
|
834
|
+
}
|