juxscript 1.1.386 → 1.1.388
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/machinery/compiler4.js +13 -0
- package/machinery/validate-jux.js +333 -0
- package/package.json +1 -1
package/machinery/compiler4.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { generateErrorCollector } from './errors.js';
|
|
8
8
|
import { autowrap } from './autowrap.js';
|
|
9
|
+
import { validateJuxFiles, printValidationResults } from './validate-jux.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -750,6 +751,18 @@ ${bodyContainers} <div id="app"></div>
|
|
|
750
751
|
fs.copyFileSync(errSrc, path.join(this.distDir, 'jux-errors.js'));
|
|
751
752
|
}
|
|
752
753
|
|
|
754
|
+
// ── Validate .jux files before compilation ──
|
|
755
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
756
|
+
const juxValidation = validateJuxFiles(this.srcDir, packageRoot);
|
|
757
|
+
const juxErrorCount = printValidationResults(juxValidation);
|
|
758
|
+
if (juxErrorCount > 0) {
|
|
759
|
+
console.error(`\n🛑 BUILD FAILED — ${juxErrorCount} .jux validation error(s)\n`);
|
|
760
|
+
return { success: false, errors: juxValidation.errors, warnings: juxValidation.warnings };
|
|
761
|
+
}
|
|
762
|
+
if (juxValidation.stats.files > 0) {
|
|
763
|
+
console.log(`✅ Validated ${juxValidation.stats.files} .jux file(s): ${juxValidation.stats.calls} calls, ${juxValidation.stats.vars} tracked vars`);
|
|
764
|
+
}
|
|
765
|
+
|
|
753
766
|
this.copyPublicFolder();
|
|
754
767
|
|
|
755
768
|
const precompileResult = this._validatePrecompile();
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates .jux files against the Jux interface and component method signatures.
|
|
3
|
+
* Shared between run-tests.js (npm test) and compiler4.js (npm run build).
|
|
4
|
+
*
|
|
5
|
+
* Returns { errors: [...], warnings: [...], stats: { files, calls, vars } }
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import path from 'path';
|
|
9
|
+
import * as acorn from 'acorn';
|
|
10
|
+
|
|
11
|
+
const RED = '\x1b[31m';
|
|
12
|
+
const RESET = '\x1b[0m';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} juxDir - Directory containing .jux files
|
|
16
|
+
* @param {string} packageRoot - Root of the jux package (contains lib/ or dist/)
|
|
17
|
+
* @returns {{ errors: Array<{file:string,line:number,message:string}>, warnings: Array, stats: {files:number,calls:number,vars:number} }}
|
|
18
|
+
*/
|
|
19
|
+
export function validateJuxFiles(juxDir, packageRoot) {
|
|
20
|
+
const errors = [];
|
|
21
|
+
const warnings = [];
|
|
22
|
+
const stats = { files: 0, calls: 0, vars: 0 };
|
|
23
|
+
|
|
24
|
+
// Find index source: prefer lib/index.ts (dev), fall back to dist/index.js (installed)
|
|
25
|
+
let INDEX_PATH = path.resolve(packageRoot, 'lib/index.ts');
|
|
26
|
+
let COMPONENTS_DIR = path.resolve(packageRoot, 'lib/components');
|
|
27
|
+
let isTypescript = true;
|
|
28
|
+
|
|
29
|
+
if (!fs.existsSync(INDEX_PATH)) {
|
|
30
|
+
INDEX_PATH = path.resolve(packageRoot, 'dist/index.js');
|
|
31
|
+
COMPONENTS_DIR = path.resolve(packageRoot, 'dist/components');
|
|
32
|
+
isTypescript = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(INDEX_PATH)) {
|
|
36
|
+
warnings.push({ file: '', line: 0, message: `Neither lib/index.ts nor dist/index.js found in ${packageRoot}` });
|
|
37
|
+
return { errors, warnings, stats };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const indexSource = fs.readFileSync(INDEX_PATH, 'utf8');
|
|
41
|
+
|
|
42
|
+
// ── Extract valid Jux interface keys ──
|
|
43
|
+
let validKeys;
|
|
44
|
+
const ifaceMatch = indexSource.match(/export\s+interface\s+Jux\s*\{([\s\S]*?)\}/);
|
|
45
|
+
if (ifaceMatch) {
|
|
46
|
+
validKeys = new Set(
|
|
47
|
+
[...ifaceMatch[1].matchAll(/^\s*(\w+)\s*:/gm)].map(m => m[1])
|
|
48
|
+
);
|
|
49
|
+
} else {
|
|
50
|
+
const objMatch = indexSource.match(/(?:const|let|var)\s+jux\s*=\s*\{([\s\S]*?)\};/);
|
|
51
|
+
if (objMatch) {
|
|
52
|
+
validKeys = new Set(
|
|
53
|
+
[...objMatch[1].matchAll(/\b(\w+)\s*[,:]/gm)].map(m => m[1])
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!validKeys || validKeys.size === 0) {
|
|
59
|
+
warnings.push({ file: INDEX_PATH, line: 0, message: 'Could not extract Jux keys from index' });
|
|
60
|
+
return { errors, warnings, stats };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Build factory→component file mapping ──
|
|
64
|
+
const factoryToFile = {};
|
|
65
|
+
const importMatches = [...indexSource.matchAll(/import\s+\{([^}]+)\}\s+from\s+['"]\.\/components\/([^'"]+)['"]/g)];
|
|
66
|
+
for (const [, names, filePath] of importMatches) {
|
|
67
|
+
const file = isTypescript ? filePath.replace(/\.js$/, '.ts') : filePath;
|
|
68
|
+
for (const name of names.split(',').map(n => n.trim())) {
|
|
69
|
+
if (name) factoryToFile[name] = file;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── Build factory→valid methods & option keys ──
|
|
74
|
+
const factoryMethods = {};
|
|
75
|
+
const factoryOptionKeys = {};
|
|
76
|
+
const processedFiles = new Set();
|
|
77
|
+
|
|
78
|
+
for (const [, componentFile] of Object.entries(factoryToFile)) {
|
|
79
|
+
if (processedFiles.has(componentFile)) continue;
|
|
80
|
+
processedFiles.add(componentFile);
|
|
81
|
+
|
|
82
|
+
const filePath = path.join(COMPONENTS_DIR, componentFile);
|
|
83
|
+
if (!fs.existsSync(filePath)) continue;
|
|
84
|
+
|
|
85
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
86
|
+
|
|
87
|
+
// Extract methods — works for both .ts and .js
|
|
88
|
+
const methods = new Set();
|
|
89
|
+
// Match methods with balanced parens in params (handles callback types like (fn: (e: Event) => void))
|
|
90
|
+
const methodRegex = /^\s{4}(\w+)\s*\(/gm;
|
|
91
|
+
let match;
|
|
92
|
+
while ((match = methodRegex.exec(source)) !== null) {
|
|
93
|
+
const name = match[1];
|
|
94
|
+
if (name === 'constructor' || name.startsWith('_')) continue;
|
|
95
|
+
// Verify it's actually a method (not just any line starting with a word and paren)
|
|
96
|
+
// by checking it's followed by a method body pattern
|
|
97
|
+
const restOfLine = source.substring(match.index + match[0].length);
|
|
98
|
+
// Skip if it looks like a keyword (if, for, while, etc.)
|
|
99
|
+
if (['if', 'for', 'while', 'switch', 'catch', 'return', 'throw', 'new', 'delete', 'typeof', 'void'].includes(name)) continue;
|
|
100
|
+
methods.add(name);
|
|
101
|
+
}
|
|
102
|
+
for (const m of source.matchAll(/^\s{4}get\s+(\w+)\s*\(/gm)) {
|
|
103
|
+
methods.add(m[1]);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extract option keys — TypeScript interfaces only
|
|
107
|
+
const optKeys = new Set();
|
|
108
|
+
if (isTypescript) {
|
|
109
|
+
for (const [, body] of source.matchAll(/interface\s+\w+Options\s*\{([^}]+)\}/gs)) {
|
|
110
|
+
for (const m of body.matchAll(/^\s*(\w+)\??:/gm)) {
|
|
111
|
+
optKeys.add(m[1]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const [fn, file] of Object.entries(factoryToFile)) {
|
|
117
|
+
if (file === componentFile) {
|
|
118
|
+
factoryMethods[fn] = methods;
|
|
119
|
+
factoryOptionKeys[fn] = optKeys;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Find .jux files ──
|
|
125
|
+
if (!fs.existsSync(juxDir)) return { errors, warnings, stats };
|
|
126
|
+
|
|
127
|
+
const juxFiles = findFilesRecursive(juxDir, '.jux');
|
|
128
|
+
stats.files = juxFiles.length;
|
|
129
|
+
|
|
130
|
+
for (const filePath of juxFiles) {
|
|
131
|
+
const source = fs.readFileSync(filePath, 'utf8');
|
|
132
|
+
const relPath = path.relative(packageRoot, filePath);
|
|
133
|
+
|
|
134
|
+
let ast;
|
|
135
|
+
try {
|
|
136
|
+
ast = acorn.parse(source, {
|
|
137
|
+
ecmaVersion: 2022,
|
|
138
|
+
sourceType: 'module',
|
|
139
|
+
allowAwaitOutsideFunction: true,
|
|
140
|
+
});
|
|
141
|
+
} catch (err) {
|
|
142
|
+
errors.push({ file: relPath, line: err.loc?.line || 0, message: `Syntax error: ${err.message}` });
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Validate jux.xxx() namespace calls ──
|
|
147
|
+
walkAST(ast, {
|
|
148
|
+
CallExpression(node) {
|
|
149
|
+
const callee = node.callee;
|
|
150
|
+
if (callee.type === 'MemberExpression' &&
|
|
151
|
+
callee.object.type === 'Identifier' &&
|
|
152
|
+
callee.object.name === 'jux' &&
|
|
153
|
+
callee.property.type === 'Identifier') {
|
|
154
|
+
stats.calls++;
|
|
155
|
+
if (!validKeys.has(callee.property.name)) {
|
|
156
|
+
errors.push({
|
|
157
|
+
file: relPath,
|
|
158
|
+
line: getLine(source, callee.start),
|
|
159
|
+
message: `jux.${callee.property.name}() does not exist on Jux interface`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ── Track variable→factory mappings ──
|
|
167
|
+
const varFactory = {};
|
|
168
|
+
for (const stmt of ast.body) {
|
|
169
|
+
if (stmt.type === 'VariableDeclaration') {
|
|
170
|
+
for (const decl of stmt.declarations) {
|
|
171
|
+
if (decl.id.type === 'Identifier' && decl.init) {
|
|
172
|
+
const factory = extractJuxFactory(decl.init);
|
|
173
|
+
if (factory) varFactory[decl.id.name] = factory;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
stats.vars += Object.keys(varFactory).length;
|
|
179
|
+
|
|
180
|
+
// ── Validate inline chains on jux.xxx() initializers ──
|
|
181
|
+
// e.g. jux.btn('id').onClick(...).abc() — abc is invalid
|
|
182
|
+
walkAST(ast, {
|
|
183
|
+
CallExpression(node) {
|
|
184
|
+
// Only check member calls: something.method()
|
|
185
|
+
if (node.callee.type !== 'MemberExpression' ||
|
|
186
|
+
node.callee.property.type !== 'Identifier') return;
|
|
187
|
+
|
|
188
|
+
const methodName = node.callee.property.name;
|
|
189
|
+
const obj = node.callee.object;
|
|
190
|
+
|
|
191
|
+
// Find the root jux.xxx factory by walking down the chain
|
|
192
|
+
const factory = extractJuxFactory(obj);
|
|
193
|
+
if (!factory) return;
|
|
194
|
+
|
|
195
|
+
// Don't flag the jux.xxx() call itself
|
|
196
|
+
if (extractJuxFactoryDirect(node)) return;
|
|
197
|
+
|
|
198
|
+
const validMethods = factoryMethods[factory];
|
|
199
|
+
if (validMethods && !validMethods.has(methodName)) {
|
|
200
|
+
errors.push({
|
|
201
|
+
file: relPath,
|
|
202
|
+
line: getLine(source, node.callee.property.start),
|
|
203
|
+
message: `.${methodName}() is not a method on jux.${factory}()`
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// ── Validate chained method calls on tracked variables ──
|
|
210
|
+
walkAST(ast, {
|
|
211
|
+
CallExpression(node) {
|
|
212
|
+
if (node.callee.type === 'MemberExpression' &&
|
|
213
|
+
node.callee.property.type === 'Identifier') {
|
|
214
|
+
const methodName = node.callee.property.name;
|
|
215
|
+
const varName = resolveRootVar(node.callee.object);
|
|
216
|
+
|
|
217
|
+
if (varName && varFactory[varName]) {
|
|
218
|
+
const factory = varFactory[varName];
|
|
219
|
+
const validMethods = factoryMethods[factory];
|
|
220
|
+
if (validMethods && !validMethods.has(methodName)) {
|
|
221
|
+
// Avoid duplicate: skip if this was already caught by inline chain check
|
|
222
|
+
if (!extractJuxFactory(node.callee.object)) {
|
|
223
|
+
errors.push({
|
|
224
|
+
file: relPath,
|
|
225
|
+
line: getLine(source, node.callee.property.start),
|
|
226
|
+
message: `.${methodName}() is not a method on jux.${factory}()`
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Validate option keys — only when we have TS interface data
|
|
234
|
+
const factory = extractJuxFactoryDirect(node);
|
|
235
|
+
if (factory && factoryOptionKeys[factory] && factoryOptionKeys[factory].size > 0 && node.arguments.length > 1) {
|
|
236
|
+
const optsArg = node.arguments[1];
|
|
237
|
+
if (optsArg && optsArg.type === 'ObjectExpression') {
|
|
238
|
+
const validOpts = factoryOptionKeys[factory];
|
|
239
|
+
for (const prop of optsArg.properties) {
|
|
240
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
241
|
+
if (!validOpts.has(prop.key.name)) {
|
|
242
|
+
errors.push({
|
|
243
|
+
file: relPath,
|
|
244
|
+
line: getLine(source, prop.key.start),
|
|
245
|
+
message: `option '${prop.key.name}' is not valid for jux.${factory}()`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return { errors, warnings, stats };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Prints validation results to console with colors.
|
|
261
|
+
* @returns {number} Number of errors
|
|
262
|
+
*/
|
|
263
|
+
export function printValidationResults(result) {
|
|
264
|
+
if (result.errors.length > 0) {
|
|
265
|
+
console.error(`\n${RED}═══ .jux Validation Errors ═══${RESET}\n`);
|
|
266
|
+
for (const err of result.errors) {
|
|
267
|
+
console.error(` ${RED}❌ ${err.file}:${err.line}: ${err.message}${RESET}`);
|
|
268
|
+
}
|
|
269
|
+
console.error('');
|
|
270
|
+
}
|
|
271
|
+
return result.errors.length;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Helpers ──
|
|
275
|
+
|
|
276
|
+
function findFilesRecursive(dir, ext) {
|
|
277
|
+
const results = [];
|
|
278
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
279
|
+
const full = path.join(dir, entry.name);
|
|
280
|
+
if (entry.isDirectory()) results.push(...findFilesRecursive(full, ext));
|
|
281
|
+
else if (entry.name.endsWith(ext)) results.push(full);
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function walkAST(node, visitors) {
|
|
287
|
+
if (!node || typeof node !== 'object') return;
|
|
288
|
+
if (Array.isArray(node)) { node.forEach(n => walkAST(n, visitors)); return; }
|
|
289
|
+
if (visitors[node.type]) visitors[node.type](node);
|
|
290
|
+
for (const key of Object.keys(node)) {
|
|
291
|
+
if (['type', 'start', 'end', 'loc', 'raw'].includes(key)) continue;
|
|
292
|
+
const child = node[key];
|
|
293
|
+
if (child && typeof child === 'object') walkAST(child, visitors);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getLine(source, pos) {
|
|
298
|
+
let line = 1;
|
|
299
|
+
for (let i = 0; i < pos; i++) { if (source[i] === '\n') line++; }
|
|
300
|
+
return line;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function extractJuxFactory(node) {
|
|
304
|
+
if (!node) return null;
|
|
305
|
+
if (node.type === 'CallExpression') {
|
|
306
|
+
const f = extractJuxFactoryDirect(node);
|
|
307
|
+
if (f) return f;
|
|
308
|
+
if (node.callee.type === 'MemberExpression') return extractJuxFactory(node.callee.object);
|
|
309
|
+
}
|
|
310
|
+
if (node.type === 'AwaitExpression') return extractJuxFactory(node.argument);
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function extractJuxFactoryDirect(node) {
|
|
315
|
+
if (node.type === 'CallExpression' &&
|
|
316
|
+
node.callee.type === 'MemberExpression' &&
|
|
317
|
+
node.callee.object.type === 'Identifier' &&
|
|
318
|
+
node.callee.object.name === 'jux' &&
|
|
319
|
+
node.callee.property.type === 'Identifier') {
|
|
320
|
+
return node.callee.property.name;
|
|
321
|
+
}
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function resolveRootVar(node) {
|
|
326
|
+
if (!node) return null;
|
|
327
|
+
if (node.type === 'Identifier') return node.name;
|
|
328
|
+
if (node.type === 'CallExpression' && node.callee.type === 'MemberExpression') {
|
|
329
|
+
return resolveRootVar(node.callee.object);
|
|
330
|
+
}
|
|
331
|
+
if (node.type === 'MemberExpression') return resolveRootVar(node.object);
|
|
332
|
+
return null;
|
|
333
|
+
}
|