Files
metabuilder/packages/shared/seed/scripts/runtime/script_executor.js
2025-12-31 13:07:24 +00:00

473 lines
13 KiB
JavaScript

/**
* JSON Script Runtime Executor (JavaScript/TypeScript)
* Executes script.json definitions at runtime without code generation
*
* Port of script_executor.lua to JavaScript
*/
/**
* Reference resolver - resolves $ref:path.to.value
* @param {any} ref - The reference value or string
* @param {Object} context - Execution context
* @returns {any} Resolved value
*/
function resolveRef(ref, context) {
if (typeof ref !== 'string' || !ref.startsWith('$ref:')) {
return ref;
}
const path = ref.substring(5); // Remove "$ref:"
const parts = path.split('.');
let value = context;
for (const part of parts) {
if (value === null || typeof value !== 'object') {
return undefined;
}
value = value[part];
}
return value;
}
/**
* Expression evaluator
* @param {any} expr - Expression to evaluate
* @param {Object} context - Execution context
* @returns {any} Evaluated result
*/
function evalExpression(expr, context) {
if (typeof expr !== 'object' || expr === null) {
return resolveRef(expr, context);
}
const exprType = expr.type;
// Literal values
if (exprType === 'string_literal') {
return expr.value;
}
if (exprType === 'number_literal') {
return expr.value;
}
if (exprType === 'boolean_literal') {
return expr.value;
}
if (exprType === 'null_literal') {
return null;
}
// Template literals
if (exprType === 'template_literal') {
const template = expr.template;
// Simple interpolation ${variable}
return template.replace(/\$\{([^}]+)\}/g, (match, varPath) => {
const ref = `$ref:local.${varPath}`;
const value = resolveRef(ref, context);
return value !== undefined ? String(value) : '';
});
}
// Binary expressions
if (exprType === 'binary_expression') {
const left = evalExpression(expr.left, context);
const right = evalExpression(expr.right, context);
const op = expr.operator;
switch (op) {
case '+': return left + right;
case '-': return left - right;
case '*': return left * right;
case '/': return left / right;
case '%': return left % right;
case '==':
case '===': return left === right;
case '!=':
case '!==': return left !== right;
case '<': return left < right;
case '>': return left > right;
case '<=': return left <= right;
case '>=': return left >= right;
default: return undefined;
}
}
// Logical expressions (short-circuit evaluation)
if (exprType === 'logical_expression') {
const op = expr.operator;
if (op === '&&' || op === 'and') {
const left = evalExpression(expr.left, context);
if (!left) return left; // Short-circuit
return evalExpression(expr.right, context);
}
if (op === '||' || op === 'or') {
const left = evalExpression(expr.left, context);
if (left) return left; // Short-circuit
return evalExpression(expr.right, context);
}
if (op === '??') {
const left = evalExpression(expr.left, context);
if (left !== null && left !== undefined) return left; // Null coalescing
return evalExpression(expr.right, context);
}
}
// Unary expressions
if (exprType === 'unary_expression') {
const arg = evalExpression(expr.argument, context);
const op = expr.operator;
switch (op) {
case '!':
case 'not': return !arg;
case '-': return -arg;
case '+': return +arg;
case '~': return ~arg; // Bitwise NOT
case 'typeof': return typeof arg;
default: return undefined;
}
}
// Conditional expression (ternary)
if (exprType === 'conditional_expression') {
const test = evalExpression(expr.test, context);
if (test) {
return evalExpression(expr.consequent, context);
} else {
return evalExpression(expr.alternate, context);
}
}
// Member access
if (exprType === 'member_access') {
const obj = evalExpression(expr.object, context);
if (obj !== null && typeof obj === 'object') {
// Computed property access
if (expr.computed) {
const prop = evalExpression(expr.property, context);
return obj[prop];
} else {
return obj[expr.property];
}
}
return undefined;
}
// Call expression
if (exprType === 'call_expression') {
const callee = resolveRef(expr.callee, context);
const args = [];
if (expr.args) {
for (const arg of expr.args) {
args.push(evalExpression(arg, context));
}
}
if (typeof callee === 'function') {
return callee(...args);
}
return undefined;
}
// Object literal
if (exprType === 'object_literal') {
const obj = {};
if (expr.properties) {
for (const [key, value] of Object.entries(expr.properties)) {
obj[key] = evalExpression(value, context);
}
}
return obj;
}
// Array literal
if (exprType === 'array_literal') {
const arr = [];
if (expr.elements) {
for (const elem of expr.elements) {
arr.push(evalExpression(elem, context));
}
}
return arr;
}
return undefined;
}
/**
* Statement executor
* @param {Object} stmt - Statement to execute
* @param {Object} context - Execution context
* @returns {Object|null} Return object or null
*/
function executeStatement(stmt, context) {
const stmtType = stmt.type;
// Variable declarations
if (stmtType === 'const_declaration' || stmtType === 'let_declaration') {
const name = stmt.name;
const value = evalExpression(stmt.value, context);
context.local_vars = context.local_vars || {};
context.local_vars[name] = value;
return null;
}
// Assignment
if (stmtType === 'assignment') {
const target = stmt.target;
const value = evalExpression(stmt.value, context);
// Handle $ref: target for complex assignments
if (typeof target === 'string' && target.startsWith('$ref:')) {
const path = target.substring(5);
const parts = path.split('.');
if (parts.length === 2 && parts[0] === 'local') {
context.local_vars = context.local_vars || {};
context.local_vars[parts[1]] = value;
} else if (parts.length === 2 && parts[0] === 'params') {
context.params[parts[1]] = value;
}
} else if (typeof target === 'string') {
context.local_vars = context.local_vars || {};
context.local_vars[target] = value;
}
return null;
}
// If statement
if (stmtType === 'if_statement') {
const condition = evalExpression(stmt.condition, context);
if (condition) {
if (stmt.then) {
for (const s of stmt.then) {
const result = executeStatement(s, context);
if (result && result.type === 'return') {
return result;
}
}
}
} else if (stmt.else) {
for (const s of stmt.else) {
const result = executeStatement(s, context);
if (result && result.type === 'return') {
return result;
}
}
}
return null;
}
// Return statement
if (stmtType === 'return') {
return {
type: 'return',
value: evalExpression(stmt.value, context)
};
}
// Try-catch
if (stmtType === 'try_catch') {
let caughtError = null;
try {
if (stmt.try) {
for (const s of stmt.try) {
const result = executeStatement(s, context);
if (result && result.type === 'return') {
return result;
}
}
}
} catch (err) {
caughtError = err;
if (stmt.catch) {
context.catch = context.catch || {};
context.catch[stmt.catch.param || 'err'] = err;
for (const s of stmt.catch.body) {
const result = executeStatement(s, context);
if (result && result.type === 'return') {
return result;
}
}
}
} finally {
if (stmt.finally) {
for (const s of stmt.finally) {
executeStatement(s, context);
}
}
}
return null;
}
// Call expression statement
if (stmtType === 'call_expression') {
evalExpression(stmt, context);
return null;
}
// For-each loop (ipairs/for...of)
if (stmtType === 'for_each_loop') {
const iterable = evalExpression(stmt.iterable, context);
if (Array.isArray(iterable)) {
context.local_vars = context.local_vars || {};
for (const item of iterable) {
context.local_vars[stmt.iterator] = item;
if (stmt.body) {
for (const s of stmt.body) {
const result = executeStatement(s, context);
if (result && result.type === 'return') {
return result;
}
}
}
}
}
return null;
}
// Comment (ignored at runtime)
if (stmtType === 'comment') {
return null;
}
return null;
}
/**
* Execute function from script.json
* @param {Object} scriptJson - Parsed script.json object
* @param {string} functionName - Name of function to execute
* @param {Array} args - Arguments to pass to function
* @returns {any} Function result
*/
function executeFunction(scriptJson, functionName, args = []) {
// Find function definition
const funcDef = (scriptJson.functions || []).find(fn => fn.name === functionName);
if (!funcDef) {
throw new Error(`Function not found: ${functionName}`);
}
// Build context
const context = {
params: {},
local_vars: {},
constants: {},
imports: {},
functions: {}
};
// Load constants
for (const constant of (scriptJson.constants || [])) {
context.constants[constant.name] = constant.value;
}
// Set parameters with defaults
for (let i = 0; i < (funcDef.params || []).length; i++) {
const param = funcDef.params[i];
let value = args[i];
// Handle default values
if (value === undefined && param.default !== undefined) {
if (typeof param.default === 'string' && param.default.startsWith('$ref:')) {
value = resolveRef(param.default, context);
} else if (typeof param.default === 'object' && param.default !== null) {
value = evalExpression(param.default, context);
} else {
value = param.default;
}
}
context.params[param.name] = value;
}
// Execute function body
for (const stmt of (funcDef.body || [])) {
const result = executeStatement(stmt, context);
if (result && result.type === 'return') {
return result.value;
}
}
return undefined;
}
/**
* Load and execute script.json file
* @param {string} jsonPath - Path to script.json file
* @param {string} functionName - Name of function to execute
* @param {Array} args - Arguments to pass to function
* @returns {Promise<any>} Function result
*/
async function loadAndExecute(jsonPath, functionName, args = []) {
const fs = await import('fs/promises');
const content = await fs.readFile(jsonPath, 'utf-8');
const scriptJson = JSON.parse(content);
return executeFunction(scriptJson, functionName, args);
}
/**
* Synchronous version of loadAndExecute for Node.js
* @param {string} jsonPath - Path to script.json file
* @param {string} functionName - Name of function to execute
* @param {Array} args - Arguments to pass to function
* @returns {any} Function result
*/
function loadAndExecuteSync(jsonPath, functionName, args = []) {
if (typeof require !== 'undefined') {
const fs = require('fs');
const content = fs.readFileSync(jsonPath, 'utf-8');
const scriptJson = JSON.parse(content);
return executeFunction(scriptJson, functionName, args);
}
throw new Error('Synchronous file loading not available in browser environment');
}
// Export for different module systems
if (typeof module !== 'undefined' && module.exports) {
// CommonJS
module.exports = {
executeFunction,
loadAndExecute,
loadAndExecuteSync,
resolveRef,
evalExpression,
executeStatement
};
}
if (typeof window !== 'undefined') {
// Browser global
window.ScriptExecutor = {
executeFunction,
loadAndExecute,
resolveRef,
evalExpression,
executeStatement
};
}
// ES6 export
export {
executeFunction,
loadAndExecute,
loadAndExecuteSync,
resolveRef,
evalExpression,
executeStatement
};
export default {
executeFunction,
loadAndExecute,
loadAndExecuteSync,
resolveRef,
evalExpression,
executeStatement
};